From 8e05ce567fe734ed4d9eb299d5f4a972a200fcf9 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Tue, 11 Sep 2018 08:46:26 +0100 Subject: [PATCH 01/78] [ML] Rename input_fields to column_names in file structure (#33568) This change tightens up the meaning of the "input_fields" field in the file structure finder output. Previously it was permitted but not calculated for JSON and XML files. Following this change the field is called "column_names" and is only permitted for delimited files. Additionally the way the column names are set for headerless delimited files is refactored to encapsulate the way they're named to one line of the code rather than having the same logic in two places. --- .../ml/filestructurefinder/FileStructure.java | 47 ++++++++++--------- .../FileStructureTests.java | 9 ++-- .../DelimitedFileStructureFinder.java | 16 ++++--- .../DelimitedFileStructureFinderTests.java | 12 ++--- 4 files changed, 45 insertions(+), 39 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/filestructurefinder/FileStructure.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/filestructurefinder/FileStructure.java index 5484f9f9902..dd508dfb36b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/filestructurefinder/FileStructure.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/filestructurefinder/FileStructure.java @@ -92,7 +92,7 @@ public class FileStructure implements ToXContentObject, Writeable { static final ParseField STRUCTURE = new ParseField("format"); static final ParseField MULTILINE_START_PATTERN = new ParseField("multiline_start_pattern"); static final ParseField EXCLUDE_LINES_PATTERN = new ParseField("exclude_lines_pattern"); - static final ParseField INPUT_FIELDS = new ParseField("input_fields"); + static final ParseField COLUMN_NAMES = new ParseField("column_names"); static final ParseField HAS_HEADER_ROW = new ParseField("has_header_row"); static final ParseField DELIMITER = new ParseField("delimiter"); static final ParseField SHOULD_TRIM_FIELDS = new ParseField("should_trim_fields"); @@ -115,7 +115,7 @@ public class FileStructure implements ToXContentObject, Writeable { PARSER.declareString((p, c) -> p.setFormat(Format.fromString(c)), STRUCTURE); PARSER.declareString(Builder::setMultilineStartPattern, MULTILINE_START_PATTERN); PARSER.declareString(Builder::setExcludeLinesPattern, EXCLUDE_LINES_PATTERN); - PARSER.declareStringArray(Builder::setInputFields, INPUT_FIELDS); + PARSER.declareStringArray(Builder::setColumnNames, COLUMN_NAMES); PARSER.declareBoolean(Builder::setHasHeaderRow, HAS_HEADER_ROW); PARSER.declareString((p, c) -> p.setDelimiter(c.charAt(0)), DELIMITER); PARSER.declareBoolean(Builder::setShouldTrimFields, SHOULD_TRIM_FIELDS); @@ -142,7 +142,7 @@ public class FileStructure implements ToXContentObject, Writeable { private final Format format; private final String multilineStartPattern; private final String excludeLinesPattern; - private final List inputFields; + private final List columnNames; private final Boolean hasHeaderRow; private final Character delimiter; private final Boolean shouldTrimFields; @@ -155,7 +155,7 @@ public class FileStructure implements ToXContentObject, Writeable { private final List explanation; public FileStructure(int numLinesAnalyzed, int numMessagesAnalyzed, String sampleStart, String charset, Boolean hasByteOrderMarker, - Format format, String multilineStartPattern, String excludeLinesPattern, List inputFields, + Format format, String multilineStartPattern, String excludeLinesPattern, List columnNames, Boolean hasHeaderRow, Character delimiter, Boolean shouldTrimFields, String grokPattern, String timestampField, List timestampFormats, boolean needClientTimezone, Map mappings, Map fieldStats, List explanation) { @@ -168,7 +168,7 @@ public class FileStructure implements ToXContentObject, Writeable { this.format = Objects.requireNonNull(format); this.multilineStartPattern = multilineStartPattern; this.excludeLinesPattern = excludeLinesPattern; - this.inputFields = (inputFields == null) ? null : Collections.unmodifiableList(new ArrayList<>(inputFields)); + this.columnNames = (columnNames == null) ? null : Collections.unmodifiableList(new ArrayList<>(columnNames)); this.hasHeaderRow = hasHeaderRow; this.delimiter = delimiter; this.shouldTrimFields = shouldTrimFields; @@ -190,7 +190,7 @@ public class FileStructure implements ToXContentObject, Writeable { format = in.readEnum(Format.class); multilineStartPattern = in.readOptionalString(); excludeLinesPattern = in.readOptionalString(); - inputFields = in.readBoolean() ? Collections.unmodifiableList(in.readList(StreamInput::readString)) : null; + columnNames = in.readBoolean() ? Collections.unmodifiableList(in.readList(StreamInput::readString)) : null; hasHeaderRow = in.readOptionalBoolean(); delimiter = in.readBoolean() ? (char) in.readVInt() : null; shouldTrimFields = in.readOptionalBoolean(); @@ -213,11 +213,11 @@ public class FileStructure implements ToXContentObject, Writeable { out.writeEnum(format); out.writeOptionalString(multilineStartPattern); out.writeOptionalString(excludeLinesPattern); - if (inputFields == null) { + if (columnNames == null) { out.writeBoolean(false); } else { out.writeBoolean(true); - out.writeCollection(inputFields, StreamOutput::writeString); + out.writeCollection(columnNames, StreamOutput::writeString); } out.writeOptionalBoolean(hasHeaderRow); if (delimiter == null) { @@ -273,8 +273,8 @@ public class FileStructure implements ToXContentObject, Writeable { return excludeLinesPattern; } - public List getInputFields() { - return inputFields; + public List getColumnNames() { + return columnNames; } public Boolean getHasHeaderRow() { @@ -335,8 +335,8 @@ public class FileStructure implements ToXContentObject, Writeable { if (excludeLinesPattern != null && excludeLinesPattern.isEmpty() == false) { builder.field(EXCLUDE_LINES_PATTERN.getPreferredName(), excludeLinesPattern); } - if (inputFields != null && inputFields.isEmpty() == false) { - builder.field(INPUT_FIELDS.getPreferredName(), inputFields); + if (columnNames != null && columnNames.isEmpty() == false) { + builder.field(COLUMN_NAMES.getPreferredName(), columnNames); } if (hasHeaderRow != null) { builder.field(HAS_HEADER_ROW.getPreferredName(), hasHeaderRow.booleanValue()); @@ -377,7 +377,7 @@ public class FileStructure implements ToXContentObject, Writeable { public int hashCode() { return Objects.hash(numLinesAnalyzed, numMessagesAnalyzed, sampleStart, charset, hasByteOrderMarker, format, - multilineStartPattern, excludeLinesPattern, inputFields, hasHeaderRow, delimiter, shouldTrimFields, grokPattern, timestampField, + multilineStartPattern, excludeLinesPattern, columnNames, hasHeaderRow, delimiter, shouldTrimFields, grokPattern, timestampField, timestampFormats, needClientTimezone, mappings, fieldStats, explanation); } @@ -402,7 +402,7 @@ public class FileStructure implements ToXContentObject, Writeable { Objects.equals(this.format, that.format) && Objects.equals(this.multilineStartPattern, that.multilineStartPattern) && Objects.equals(this.excludeLinesPattern, that.excludeLinesPattern) && - Objects.equals(this.inputFields, that.inputFields) && + Objects.equals(this.columnNames, that.columnNames) && Objects.equals(this.hasHeaderRow, that.hasHeaderRow) && Objects.equals(this.delimiter, that.delimiter) && Objects.equals(this.shouldTrimFields, that.shouldTrimFields) && @@ -424,7 +424,7 @@ public class FileStructure implements ToXContentObject, Writeable { private Format format; private String multilineStartPattern; private String excludeLinesPattern; - private List inputFields; + private List columnNames; private Boolean hasHeaderRow; private Character delimiter; private Boolean shouldTrimFields; @@ -484,8 +484,8 @@ public class FileStructure implements ToXContentObject, Writeable { return this; } - public Builder setInputFields(List inputFields) { - this.inputFields = inputFields; + public Builder setColumnNames(List columnNames) { + this.columnNames = columnNames; return this; } @@ -573,6 +573,9 @@ public class FileStructure implements ToXContentObject, Writeable { } // $FALL-THROUGH$ case XML: + if (columnNames != null) { + throw new IllegalArgumentException("Column names may not be specified for [" + format + "] structures."); + } if (hasHeaderRow != null) { throw new IllegalArgumentException("Has header row may not be specified for [" + format + "] structures."); } @@ -584,8 +587,8 @@ public class FileStructure implements ToXContentObject, Writeable { } break; case DELIMITED: - if (inputFields == null || inputFields.isEmpty()) { - throw new IllegalArgumentException("Input fields must be specified for [" + format + "] structures."); + if (columnNames == null || columnNames.isEmpty()) { + throw new IllegalArgumentException("Column names must be specified for [" + format + "] structures."); } if (hasHeaderRow == null) { throw new IllegalArgumentException("Has header row must be specified for [" + format + "] structures."); @@ -598,8 +601,8 @@ public class FileStructure implements ToXContentObject, Writeable { } break; case SEMI_STRUCTURED_TEXT: - if (inputFields != null) { - throw new IllegalArgumentException("Input fields may not be specified for [" + format + "] structures."); + if (columnNames != null) { + throw new IllegalArgumentException("Column names may not be specified for [" + format + "] structures."); } if (hasHeaderRow != null) { throw new IllegalArgumentException("Has header row may not be specified for [" + format + "] structures."); @@ -635,7 +638,7 @@ public class FileStructure implements ToXContentObject, Writeable { } return new FileStructure(numLinesAnalyzed, numMessagesAnalyzed, sampleStart, charset, hasByteOrderMarker, format, - multilineStartPattern, excludeLinesPattern, inputFields, hasHeaderRow, delimiter, shouldTrimFields, grokPattern, + multilineStartPattern, excludeLinesPattern, columnNames, hasHeaderRow, delimiter, shouldTrimFields, grokPattern, timestampField, timestampFormats, needClientTimezone, mappings, fieldStats, explanation); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/filestructurefinder/FileStructureTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/filestructurefinder/FileStructureTests.java index 6dcf6751965..e09b9e3f91e 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/filestructurefinder/FileStructureTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/filestructurefinder/FileStructureTests.java @@ -50,18 +50,17 @@ public class FileStructureTests extends AbstractSerializingTestCase headerInfo = findHeaderFromSample(explanation, rows); boolean isHeaderInFile = headerInfo.v1(); String[] header = headerInfo.v2(); - String[] headerWithNamedBlanks = new String[header.length]; + // The column names are the header names but with blanks named column1, column2, etc. + String[] columnNames = new String[header.length]; for (int i = 0; i < header.length; ++i) { - String rawHeader = header[i].isEmpty() ? "column" + (i + 1) : header[i]; - headerWithNamedBlanks[i] = trimFields ? rawHeader.trim() : rawHeader; + assert header[i] != null; + String rawHeader = trimFields ? header[i].trim() : header[i]; + columnNames[i] = rawHeader.isEmpty() ? "column" + (i + 1) : rawHeader; } List sampleLines = Arrays.asList(sample.split("\n")); @@ -63,7 +65,7 @@ public class DelimitedFileStructureFinder implements FileStructureFinder { List row = rows.get(index); int lineNumber = lineNumbers.get(index); Map sampleRecord = new LinkedHashMap<>(); - Util.filterListToMap(sampleRecord, headerWithNamedBlanks, + Util.filterListToMap(sampleRecord, columnNames, trimFields ? row.stream().map(String::trim).collect(Collectors.toList()) : row); sampleRecords.add(sampleRecord); sampleMessages.add( @@ -82,7 +84,7 @@ public class DelimitedFileStructureFinder implements FileStructureFinder { .setNumMessagesAnalyzed(sampleRecords.size()) .setHasHeaderRow(isHeaderInFile) .setDelimiter(delimiter) - .setInputFields(Arrays.stream(headerWithNamedBlanks).collect(Collectors.toList())); + .setColumnNames(Arrays.stream(columnNames).collect(Collectors.toList())); if (trimFields) { structureBuilder.setShouldTrimFields(true); @@ -225,7 +227,9 @@ public class DelimitedFileStructureFinder implements FileStructureFinder { // SuperCSV will put nulls in the header if any columns don't have names, but empty strings are better for us return new Tuple<>(true, firstRow.stream().map(field -> (field == null) ? "" : field).toArray(String[]::new)); } else { - return new Tuple<>(false, IntStream.rangeClosed(1, firstRow.size()).mapToObj(num -> "column" + num).toArray(String[]::new)); + String[] dummyHeader = new String[firstRow.size()]; + Arrays.fill(dummyHeader, ""); + return new Tuple<>(false, dummyHeader); } } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinderTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinderTests.java index 6d1f039399e..4e692d58391 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinderTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinderTests.java @@ -45,7 +45,7 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { assertEquals(Character.valueOf(','), structure.getDelimiter()); assertTrue(structure.getHasHeaderRow()); assertNull(structure.getShouldTrimFields()); - assertEquals(Arrays.asList("time", "message"), structure.getInputFields()); + assertEquals(Arrays.asList("time", "message"), structure.getColumnNames()); assertNull(structure.getGrokPattern()); assertEquals("time", structure.getTimestampField()); assertEquals(Collections.singletonList("ISO8601"), structure.getTimestampFormats()); @@ -76,7 +76,7 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { assertEquals(Character.valueOf(','), structure.getDelimiter()); assertTrue(structure.getHasHeaderRow()); assertNull(structure.getShouldTrimFields()); - assertEquals(Arrays.asList("message", "time", "count"), structure.getInputFields()); + assertEquals(Arrays.asList("message", "time", "count"), structure.getColumnNames()); assertNull(structure.getGrokPattern()); assertEquals("time", structure.getTimestampField()); assertEquals(Collections.singletonList("ISO8601"), structure.getTimestampFormats()); @@ -114,7 +114,7 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { assertNull(structure.getShouldTrimFields()); assertEquals(Arrays.asList("VendorID", "tpep_pickup_datetime", "tpep_dropoff_datetime", "passenger_count", "trip_distance", "RatecodeID", "store_and_fwd_flag", "PULocationID", "DOLocationID", "payment_type", "fare_amount", "extra", "mta_tax", - "tip_amount", "tolls_amount", "improvement_surcharge", "total_amount", "column18", "column19"), structure.getInputFields()); + "tip_amount", "tolls_amount", "improvement_surcharge", "total_amount", "column18", "column19"), structure.getColumnNames()); assertNull(structure.getGrokPattern()); assertEquals("tpep_pickup_datetime", structure.getTimestampField()); assertEquals(Collections.singletonList("YYYY-MM-dd HH:mm:ss"), structure.getTimestampFormats()); @@ -152,7 +152,7 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { assertNull(structure.getShouldTrimFields()); assertEquals(Arrays.asList("VendorID", "tpep_pickup_datetime", "tpep_dropoff_datetime", "passenger_count", "trip_distance", "RatecodeID", "store_and_fwd_flag", "PULocationID", "DOLocationID", "payment_type", "fare_amount", "extra", "mta_tax", - "tip_amount", "tolls_amount", "improvement_surcharge", "total_amount"), structure.getInputFields()); + "tip_amount", "tolls_amount", "improvement_surcharge", "total_amount"), structure.getColumnNames()); assertNull(structure.getGrokPattern()); assertEquals("tpep_pickup_datetime", structure.getTimestampField()); assertEquals(Collections.singletonList("YYYY-MM-dd HH:mm:ss"), structure.getTimestampFormats()); @@ -183,7 +183,7 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { assertEquals(Character.valueOf(','), structure.getDelimiter()); assertTrue(structure.getHasHeaderRow()); assertNull(structure.getShouldTrimFields()); - assertEquals(Arrays.asList("pos_id", "trip_id", "latitude", "longitude", "altitude", "timestamp"), structure.getInputFields()); + assertEquals(Arrays.asList("pos_id", "trip_id", "latitude", "longitude", "altitude", "timestamp"), structure.getColumnNames()); assertNull(structure.getGrokPattern()); assertEquals("timestamp", structure.getTimestampField()); assertEquals(Collections.singletonList("YYYY-MM-dd HH:mm:ss.SSSSSS"), structure.getTimestampFormats()); @@ -213,7 +213,7 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { DelimitedFileStructureFinder.readRows(withoutHeader, CsvPreference.EXCEL_PREFERENCE).v1()); assertFalse(header.v1()); - assertThat(header.v2(), arrayContaining("column1", "column2", "column3", "column4")); + assertThat(header.v2(), arrayContaining("", "", "", "")); } public void testLevenshteinDistance() { From a55fa4fd6b145ce9da849834df9abe19307019f5 Mon Sep 17 00:00:00 2001 From: Andrei Stefan Date: Tue, 11 Sep 2018 11:00:56 +0300 Subject: [PATCH 02/78] Fix Replace function. Adds more tests to all string functions. (#33478) --- .../function/scalar/string/Replace.java | 2 +- .../main/resources/string-functions.sql-spec | 127 ++++++++++++++++++ 2 files changed, 128 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Replace.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Replace.java index 9325986ac1f..3834b16ff1e 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Replace.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/Replace.java @@ -22,7 +22,7 @@ import java.util.Locale; import static java.lang.String.format; import static org.elasticsearch.xpack.sql.expression.function.scalar.script.ParamsBuilder.paramsBuilder; import static org.elasticsearch.xpack.sql.expression.function.scalar.script.ScriptTemplate.formatTemplate; -import static org.elasticsearch.xpack.sql.expression.function.scalar.string.SubstringFunctionProcessor.doProcess; +import static org.elasticsearch.xpack.sql.expression.function.scalar.string.ReplaceFunctionProcessor.doProcess; /** * Search the source string for occurrences of the pattern, and replace with the replacement string. diff --git a/x-pack/qa/sql/src/main/resources/string-functions.sql-spec b/x-pack/qa/sql/src/main/resources/string-functions.sql-spec index 15bb6dea935..c0b0430b278 100644 --- a/x-pack/qa/sql/src/main/resources/string-functions.sql-spec +++ b/x-pack/qa/sql/src/main/resources/string-functions.sql-spec @@ -1,5 +1,6 @@ stringAscii SELECT ASCII(first_name) s FROM "test_emp" WHERE emp_no < 10010 ORDER BY emp_no; + stringChar SELECT CHAR(emp_no % 10000) m, first_name FROM "test_emp" WHERE emp_no < 10010 ORDER BY emp_no; @@ -9,6 +10,9 @@ SELECT emp_no, ASCII(first_name) a FROM "test_emp" WHERE ASCII(first_name) < 100 stringAsciiEqualsConstant SELECT emp_no, ASCII(first_name) a, first_name name FROM "test_emp" WHERE ASCII(first_name) = 65 ORDER BY emp_no; +stringAsciiInline +SELECT ASCII('E') e; + //https://github.com/elastic/elasticsearch/issues/31863 //stringSelectConstantAsciiEqualsConstant //SELECT ASCII('A') = 65 a FROM "test_emp" WHERE ASCII('A') = 65 ORDER BY emp_no; @@ -16,12 +20,105 @@ SELECT emp_no, ASCII(first_name) a, first_name name FROM "test_emp" WHERE ASCII( stringCharFilter SELECT emp_no, CHAR(emp_no % 10000) m FROM "test_emp" WHERE CHAR(emp_no % 10000) = 'A'; +stringSelectCharInline1 +SELECT CHAR(250) c; + +stringSelectCharInline2 +SELECT CHAR(2) c; + +charLengthInline1 +SELECT CAST(CHAR_LENGTH('Elasticsearch') AS INT) charlength; + +charLengthInline2 +SELECT CAST(CHAR_LENGTH(' Elasticsearch ') AS INT) charlength; + +charLengthInline3 +SELECT CAST(CHAR_LENGTH('') AS INT) charlength; + +concatInline1 +SELECT CONCAT('Elastic','search') concat; + +concatInline2 +SELECT CONCAT(CONCAT('Lucene And ', 'Elastic'),'search') concat; + +concatInline3 +SELECT CONCAT(CONCAT('Lucene And ', 'Elastic'),CONCAT('search','')) concat; + lcaseFilter SELECT LCASE(first_name) lc, CHAR(ASCII(LCASE(first_name))) chr FROM "test_emp" WHERE CHAR(ASCII(LCASE(first_name))) = 'a'; +lcaseInline1 +SELECT LCASE('') L; + +lcaseInline2 +SELECT LCASE('ElAsTiC fantastic') lower; + +leftInline1 +SELECT LEFT('Elasticsearch', 7) leftchars; + +leftInline2 +SELECT LEFT('Elasticsearch', 1) leftchars; + +leftInline3 +SELECT LEFT('Elasticsearch', 25) leftchars; + +leftInline4 +SELECT LEFT('Elasticsearch', LENGTH('abcdefghijklmnop')) leftchars; + ltrimFilter SELECT LTRIM(first_name) lt FROM "test_emp" WHERE LTRIM(first_name) = 'Bob'; +ltrimInline1 +SELECT LTRIM(' Elastic ') trimmed; + +ltrimInline2 +SELECT LTRIM(' ') trimmed; + +locateInline1 +SELECT LOCATE('a', 'Elasticsearch', 8) location; + +locateInline2 +SELECT LOCATE('a', 'Elasticsearch') location; + +locateInline3 +SELECT LOCATE('x', 'Elasticsearch') location; + +insertInline1 +SELECT INSERT('Insert [here] your comment!', 8, 6, '(random thoughts about Elasticsearch)') ins; + +insertInline2 +SELECT INSERT('Insert [here] your comment!', 8, 20, '(random thoughts about Elasticsearch)') ins; + +insertInline3 +SELECT INSERT('Insert [here] your comment!', 8, 19, '(random thoughts about Elasticsearch)') ins; + +positionInline1 +SELECT POSITION('a','Elasticsearch') pos; + +positionInline2 +SELECT POSITION('x','Elasticsearch') pos; + +repeatInline1 +SELECT REPEAT('Elastic',2) rep; + +repeatInline2 +SELECT REPEAT('Elastic',1) rep; + +replaceInline1 +SELECT REPLACE('Elasticsearch','sea','A') repl; + +replaceInline2 +SELECT REPLACE('Elasticsearch','x','A') repl; + +rightInline1 +SELECT RIGHT('Elasticsearch', LENGTH('Search')) rightchars; + +rightInline2 +SELECT RIGHT(CONCAT('Elastic','search'), LENGTH('Search')) rightchars; + +rightInline3 +SELECT RIGHT('Elasticsearch', 0) rightchars; + // Unsupported yet // Functions combined with 'LIKE' should perform the match inside a Painless script, whereas at the moment it's handled as a regular `match` query in ES. //ltrimFilterWithLike @@ -30,15 +127,45 @@ SELECT LTRIM(first_name) lt FROM "test_emp" WHERE LTRIM(first_name) = 'Bob'; rtrimFilter SELECT RTRIM(first_name) rt FROM "test_emp" WHERE RTRIM(first_name) = 'Johnny'; +rtrimInline1 +SELECT RTRIM(' Elastic ') trimmed; + +rtrimInline2 +SELECT RTRIM(' ') trimmed; + spaceFilter SELECT SPACE(languages) spaces, languages FROM "test_emp" WHERE SPACE(languages) = ' '; spaceFilterWithLengthFunctions SELECT SPACE(languages) spaces, languages, first_name FROM "test_emp" WHERE CHAR_LENGTH(SPACE(languages)) = 3 ORDER BY first_name; +spaceInline1 +SELECT SPACE(5) space; + +spaceInline1 +SELECT SPACE(0) space; + +substringInline1 +SELECT SUBSTRING('Elasticsearch', 1, 7) sub; + +substringInline2 +SELECT SUBSTRING('Elasticsearch', 1, 15) sub; + +substringInline3 +SELECT SUBSTRING('Elasticsearch', 10, 10) sub; + ucaseFilter SELECT UCASE(gender) uppercased, COUNT(*) count FROM "test_emp" WHERE UCASE(gender) = 'F' GROUP BY UCASE(gender); +ucaseInline1 +SELECT UCASE('ElAsTiC') upper; + +ucaseInline2 +SELECT UCASE('') upper; + +ucaseInline3 +SELECT UCASE(' elastic ') upper; + // // Group and order by // From f598297f55fa60df9fdefa4f34574d485b600b20 Mon Sep 17 00:00:00 2001 From: Alan Woodward Date: Tue, 11 Sep 2018 09:16:39 +0100 Subject: [PATCH 03/78] Add predicate_token_filter (#33431) This allows users to filter out tokens from a TokenStream using painless scripts, instead of having to write specialised Java code and packaging it up into a plugin. The commit also refactors the AnalysisPredicateScript.Token class so that it wraps and makes read-only an AttributeSource. --- docs/reference/analysis/tokenfilters.asciidoc | 2 + .../predicate-tokenfilter.asciidoc | 79 ++++++++++++++++ .../common/AnalysisPredicateScript.java | 56 ++++++++---- .../analysis/common/CommonAnalysisPlugin.java | 2 + .../PredicateTokenFilterScriptFactory.java | 73 +++++++++++++++ .../ScriptedConditionTokenFilterFactory.java | 47 ++++------ .../PredicateTokenScriptFilterTests.java | 89 +++++++++++++++++++ .../analysis-common/60_analysis_scripting.yml | 37 +++++++- 8 files changed, 341 insertions(+), 44 deletions(-) create mode 100644 docs/reference/analysis/tokenfilters/predicate-tokenfilter.asciidoc create mode 100644 modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/PredicateTokenFilterScriptFactory.java create mode 100644 modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/PredicateTokenScriptFilterTests.java diff --git a/docs/reference/analysis/tokenfilters.asciidoc b/docs/reference/analysis/tokenfilters.asciidoc index f531bc5d0e9..41bb9d38afb 100644 --- a/docs/reference/analysis/tokenfilters.asciidoc +++ b/docs/reference/analysis/tokenfilters.asciidoc @@ -37,6 +37,8 @@ include::tokenfilters/multiplexer-tokenfilter.asciidoc[] include::tokenfilters/condition-tokenfilter.asciidoc[] +include::tokenfilters/predicate-tokenfilter.asciidoc[] + include::tokenfilters/stemmer-tokenfilter.asciidoc[] include::tokenfilters/stemmer-override-tokenfilter.asciidoc[] diff --git a/docs/reference/analysis/tokenfilters/predicate-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/predicate-tokenfilter.asciidoc new file mode 100644 index 00000000000..bebf7bd80f2 --- /dev/null +++ b/docs/reference/analysis/tokenfilters/predicate-tokenfilter.asciidoc @@ -0,0 +1,79 @@ +[[analysis-predicatefilter-tokenfilter]] +=== Predicate Token Filter Script + +The predicate_token_filter token filter takes a predicate script, and removes tokens that do +not match the predicate. + +[float] +=== Options +[horizontal] +script:: a predicate script that determines whether or not the current token will +be emitted. Note that only inline scripts are supported. + +[float] +=== Settings example + +You can set it up like: + +[source,js] +-------------------------------------------------- +PUT /condition_example +{ + "settings" : { + "analysis" : { + "analyzer" : { + "my_analyzer" : { + "tokenizer" : "standard", + "filter" : [ "my_script_filter" ] + } + }, + "filter" : { + "my_script_filter" : { + "type" : "predicate_token_filter", + "script" : { + "source" : "token.getTerm().length() > 5" <1> + } + } + } + } + } +} +-------------------------------------------------- +// CONSOLE + +<1> This will emit tokens that are more than 5 characters long + +And test it like: + +[source,js] +-------------------------------------------------- +POST /condition_example/_analyze +{ + "analyzer" : "my_analyzer", + "text" : "What Flapdoodle" +} +-------------------------------------------------- +// CONSOLE +// TEST[continued] + +And it'd respond: + +[source,js] +-------------------------------------------------- +{ + "tokens": [ + { + "token": "Flapdoodle", <1> + "start_offset": 5, + "end_offset": 15, + "type": "", + "position": 1 <2> + } + ] +} +-------------------------------------------------- +// TESTRESPONSE + +<1> The token 'What' has been removed from the tokenstream because it does not +match the predicate. +<2> The position and offset values are unaffected by the removal of earlier tokens \ No newline at end of file diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/AnalysisPredicateScript.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/AnalysisPredicateScript.java index 7de588a958c..3bda6f393bf 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/AnalysisPredicateScript.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/AnalysisPredicateScript.java @@ -19,6 +19,13 @@ package org.elasticsearch.analysis.common; +import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; +import org.apache.lucene.analysis.tokenattributes.KeywordAttribute; +import org.apache.lucene.analysis.tokenattributes.OffsetAttribute; +import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute; +import org.apache.lucene.analysis.tokenattributes.PositionLengthAttribute; +import org.apache.lucene.analysis.tokenattributes.TypeAttribute; +import org.apache.lucene.util.AttributeSource; import org.elasticsearch.script.ScriptContext; /** @@ -30,21 +37,40 @@ public abstract class AnalysisPredicateScript { * Encapsulation of the state of the current token */ public static class Token { - public CharSequence term; - public int pos; - public int posInc; - public int posLen; - public int startOffset; - public int endOffset; - public String type; - public boolean isKeyword; + + private final CharTermAttribute termAtt; + private final PositionIncrementAttribute posIncAtt; + private final PositionLengthAttribute posLenAtt; + private final OffsetAttribute offsetAtt; + private final TypeAttribute typeAtt; + private final KeywordAttribute keywordAtt; + + // posInc is always 1 at the beginning of a tokenstream and the convention + // from the _analyze endpoint is that tokenstream positions are 0-based + private int pos = -1; + + /** + * Create a token exposing values from an AttributeSource + */ + public Token(AttributeSource source) { + this.termAtt = source.addAttribute(CharTermAttribute.class); + this.posIncAtt = source.addAttribute(PositionIncrementAttribute.class); + this.posLenAtt = source.addAttribute(PositionLengthAttribute.class); + this.offsetAtt = source.addAttribute(OffsetAttribute.class); + this.typeAtt = source.addAttribute(TypeAttribute.class); + this.keywordAtt = source.addAttribute(KeywordAttribute.class); + } + + public void updatePosition() { + this.pos = this.pos + posIncAtt.getPositionIncrement(); + } public CharSequence getTerm() { - return term; + return termAtt; } public int getPositionIncrement() { - return posInc; + return posIncAtt.getPositionIncrement(); } public int getPosition() { @@ -52,23 +78,23 @@ public abstract class AnalysisPredicateScript { } public int getPositionLength() { - return posLen; + return posLenAtt.getPositionLength(); } public int getStartOffset() { - return startOffset; + return offsetAtt.startOffset(); } public int getEndOffset() { - return endOffset; + return offsetAtt.endOffset(); } public String getType() { - return type; + return typeAtt.type(); } public boolean isKeyword() { - return isKeyword; + return keywordAtt.isKeyword(); } } diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CommonAnalysisPlugin.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CommonAnalysisPlugin.java index 75ebade0b12..175935258ad 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CommonAnalysisPlugin.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CommonAnalysisPlugin.java @@ -264,6 +264,8 @@ public class CommonAnalysisPlugin extends Plugin implements AnalysisPlugin, Scri filters.put("pattern_replace", requiresAnalysisSettings(PatternReplaceTokenFilterFactory::new)); filters.put("persian_normalization", PersianNormalizationFilterFactory::new); filters.put("porter_stem", PorterStemTokenFilterFactory::new); + filters.put("predicate_token_filter", + requiresAnalysisSettings((i, e, n, s) -> new PredicateTokenFilterScriptFactory(i, n, s, scriptService.get()))); filters.put("remove_duplicates", RemoveDuplicatesTokenFilterFactory::new); filters.put("reverse", ReverseTokenFilterFactory::new); filters.put("russian_stem", RussianStemTokenFilterFactory::new); diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/PredicateTokenFilterScriptFactory.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/PredicateTokenFilterScriptFactory.java new file mode 100644 index 00000000000..84f4bb48706 --- /dev/null +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/PredicateTokenFilterScriptFactory.java @@ -0,0 +1,73 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.analysis.common; + +import org.apache.lucene.analysis.FilteringTokenFilter; +import org.apache.lucene.analysis.TokenStream; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.analysis.AbstractTokenFilterFactory; +import org.elasticsearch.script.Script; +import org.elasticsearch.script.ScriptService; +import org.elasticsearch.script.ScriptType; + +import java.io.IOException; + +/** + * A factory for creating FilteringTokenFilters that determine whether or not to + * accept their underlying token by consulting a script + */ +public class PredicateTokenFilterScriptFactory extends AbstractTokenFilterFactory { + + private final AnalysisPredicateScript.Factory factory; + + public PredicateTokenFilterScriptFactory(IndexSettings indexSettings, String name, Settings settings, ScriptService scriptService) { + super(indexSettings, name, settings); + Settings scriptSettings = settings.getAsSettings("script"); + Script script = Script.parse(scriptSettings); + if (script.getType() != ScriptType.INLINE) { + throw new IllegalArgumentException("Cannot use stored scripts in tokenfilter [" + name + "]"); + } + this.factory = scriptService.compile(script, AnalysisPredicateScript.CONTEXT); + } + + @Override + public TokenStream create(TokenStream tokenStream) { + return new ScriptFilteringTokenFilter(tokenStream, factory.newInstance()); + } + + private static class ScriptFilteringTokenFilter extends FilteringTokenFilter { + + final AnalysisPredicateScript script; + final AnalysisPredicateScript.Token token; + + ScriptFilteringTokenFilter(TokenStream in, AnalysisPredicateScript script) { + super(in); + this.script = script; + this.token = new AnalysisPredicateScript.Token(this); + } + + @Override + protected boolean accept() throws IOException { + token.updatePosition(); + return script.execute(token); + } + } +} diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/ScriptedConditionTokenFilterFactory.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/ScriptedConditionTokenFilterFactory.java index cf7fd5b047a..56f60bb874a 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/ScriptedConditionTokenFilterFactory.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/ScriptedConditionTokenFilterFactory.java @@ -21,12 +21,6 @@ package org.elasticsearch.analysis.common; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.miscellaneous.ConditionalTokenFilter; -import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; -import org.apache.lucene.analysis.tokenattributes.KeywordAttribute; -import org.apache.lucene.analysis.tokenattributes.OffsetAttribute; -import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute; -import org.apache.lucene.analysis.tokenattributes.PositionLengthAttribute; -import org.apache.lucene.analysis.tokenattributes.TypeAttribute; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.analysis.AbstractTokenFilterFactory; @@ -36,6 +30,7 @@ import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptService; import org.elasticsearch.script.ScriptType; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -76,30 +71,26 @@ public class ScriptedConditionTokenFilterFactory extends AbstractTokenFilterFact } return in; }; - AnalysisPredicateScript script = factory.newInstance(); - final AnalysisPredicateScript.Token token = new AnalysisPredicateScript.Token(); - return new ConditionalTokenFilter(tokenStream, filter) { + return new ScriptedConditionTokenFilter(tokenStream, filter, factory.newInstance()); + } - CharTermAttribute termAtt = addAttribute(CharTermAttribute.class); - PositionIncrementAttribute posIncAtt = addAttribute(PositionIncrementAttribute.class); - PositionLengthAttribute posLenAtt = addAttribute(PositionLengthAttribute.class); - OffsetAttribute offsetAtt = addAttribute(OffsetAttribute.class); - TypeAttribute typeAtt = addAttribute(TypeAttribute.class); - KeywordAttribute keywordAtt = addAttribute(KeywordAttribute.class); + private static class ScriptedConditionTokenFilter extends ConditionalTokenFilter { - @Override - protected boolean shouldFilter() { - token.term = termAtt; - token.posInc = posIncAtt.getPositionIncrement(); - token.pos += token.posInc; - token.posLen = posLenAtt.getPositionLength(); - token.startOffset = offsetAtt.startOffset(); - token.endOffset = offsetAtt.endOffset(); - token.type = typeAtt.type(); - token.isKeyword = keywordAtt.isKeyword(); - return script.execute(token); - } - }; + private final AnalysisPredicateScript script; + private final AnalysisPredicateScript.Token token; + + ScriptedConditionTokenFilter(TokenStream input, Function inputFactory, + AnalysisPredicateScript script) { + super(input, inputFactory); + this.script = script; + this.token = new AnalysisPredicateScript.Token(this); + } + + @Override + protected boolean shouldFilter() throws IOException { + token.updatePosition(); + return script.execute(token); + } } @Override diff --git a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/PredicateTokenScriptFilterTests.java b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/PredicateTokenScriptFilterTests.java new file mode 100644 index 00000000000..18afbdcecb3 --- /dev/null +++ b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/PredicateTokenScriptFilterTests.java @@ -0,0 +1,89 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.analysis.common; + +import org.elasticsearch.Version; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.analysis.IndexAnalyzers; +import org.elasticsearch.index.analysis.NamedAnalyzer; +import org.elasticsearch.indices.analysis.AnalysisModule; +import org.elasticsearch.script.Script; +import org.elasticsearch.script.ScriptContext; +import org.elasticsearch.script.ScriptService; +import org.elasticsearch.test.ESTokenStreamTestCase; +import org.elasticsearch.test.IndexSettingsModule; + +import java.io.IOException; +import java.util.Collections; + +public class PredicateTokenScriptFilterTests extends ESTokenStreamTestCase { + + public void testSimpleFilter() throws IOException { + Settings settings = Settings.builder() + .put(Environment.PATH_HOME_SETTING.getKey(), createTempDir().toString()) + .build(); + Settings indexSettings = Settings.builder() + .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) + .put("index.analysis.filter.f.type", "predicate_token_filter") + .put("index.analysis.filter.f.script.source", "token.getTerm().length() > 5") + .put("index.analysis.analyzer.myAnalyzer.type", "custom") + .put("index.analysis.analyzer.myAnalyzer.tokenizer", "standard") + .putList("index.analysis.analyzer.myAnalyzer.filter", "f") + .build(); + IndexSettings idxSettings = IndexSettingsModule.newIndexSettings("index", indexSettings); + + AnalysisPredicateScript.Factory factory = () -> new AnalysisPredicateScript() { + @Override + public boolean execute(Token token) { + return token.getTerm().length() > 5; + } + }; + + @SuppressWarnings("unchecked") + ScriptService scriptService = new ScriptService(indexSettings, Collections.emptyMap(), Collections.emptyMap()){ + @Override + public FactoryType compile(Script script, ScriptContext context) { + assertEquals(context, AnalysisPredicateScript.CONTEXT); + assertEquals(new Script("token.getTerm().length() > 5"), script); + return (FactoryType) factory; + } + }; + + CommonAnalysisPlugin plugin = new CommonAnalysisPlugin(); + plugin.createComponents(null, null, null, null, scriptService, null, null, null, null); + AnalysisModule module + = new AnalysisModule(TestEnvironment.newEnvironment(settings), Collections.singletonList(plugin)); + + IndexAnalyzers analyzers = module.getAnalysisRegistry().build(idxSettings); + + try (NamedAnalyzer analyzer = analyzers.get("myAnalyzer")) { + assertNotNull(analyzer); + assertAnalyzesTo(analyzer, "Vorsprung Durch Technik", new String[]{ + "Vorsprung", "Technik" + }); + } + + } + +} diff --git a/modules/analysis-common/src/test/resources/rest-api-spec/test/analysis-common/60_analysis_scripting.yml b/modules/analysis-common/src/test/resources/rest-api-spec/test/analysis-common/60_analysis_scripting.yml index 4305e5db0af..2015fe31fcc 100644 --- a/modules/analysis-common/src/test/resources/rest-api-spec/test/analysis-common/60_analysis_scripting.yml +++ b/modules/analysis-common/src/test/resources/rest-api-spec/test/analysis-common/60_analysis_scripting.yml @@ -28,9 +28,44 @@ - type: condition filter: [ "lowercase" ] script: - source: "token.position > 1 && token.positionIncrement > 0 && token.startOffset > 0 && token.endOffset > 0 && (token.positionLength == 1 || token.type == \"a\" || token.keyword)" + source: "token.position >= 1 && token.positionIncrement > 0 && token.startOffset > 0 && token.endOffset > 0 && (token.positionLength == 1 || token.type == \"a\" || token.keyword)" - length: { tokens: 3 } - match: { tokens.0.token: "Vorsprung" } - match: { tokens.1.token: "durch" } - match: { tokens.2.token: "technik" } + +--- +"script_filter": + - do: + indices.analyze: + body: + text: "Vorsprung Durch Technik" + tokenizer: "whitespace" + filter: + - type: predicate_token_filter + script: + source: "token.term.length() > 5" + + - length: { tokens: 2 } + - match: { tokens.0.token: "Vorsprung" } + - match: { tokens.1.token: "Technik" } + +--- +"script_filter_position": + - do: + indices.analyze: + body: + text: "a b c d e f g h" + tokenizer: "whitespace" + filter: + - type: predicate_token_filter + script: + source: "token.position >= 4" + + - length: { tokens: 4 } + - match: { tokens.0.token: "e" } + - match: { tokens.1.token: "f" } + - match: { tokens.2.token: "g" } + - match: { tokens.3.token: "h" } + From a3e1f1e46f444e656f1eed62736c85e0c2d903e6 Mon Sep 17 00:00:00 2001 From: Andrei Stefan Date: Tue, 11 Sep 2018 14:35:34 +0300 Subject: [PATCH 04/78] SQL: Adds MONTHNAME, DAYNAME and QUARTER functions (#33411) * Added monthname, dayname and quarter functions * Updated docs tests with the new functions --- .../expression/function/FunctionRegistry.java | 18 ++-- .../function/scalar/Processors.java | 6 +- .../scalar/datetime/BaseDateTimeFunction.java | 70 +++++++++++++ .../datetime/BaseDateTimeProcessor.java | 59 +++++++++++ .../scalar/datetime/DateTimeFunction.java | 67 ++----------- .../scalar/datetime/DateTimeProcessor.java | 38 ++----- .../function/scalar/datetime/DayName.java | 49 ++++++++++ .../function/scalar/datetime/DayOfMonth.java | 2 +- .../function/scalar/datetime/DayOfWeek.java | 2 +- .../function/scalar/datetime/DayOfYear.java | 2 +- .../function/scalar/datetime/HourOfDay.java | 2 +- .../function/scalar/datetime/MinuteOfDay.java | 2 +- .../scalar/datetime/MinuteOfHour.java | 2 +- .../function/scalar/datetime/MonthName.java | 50 ++++++++++ .../function/scalar/datetime/MonthOfYear.java | 2 +- .../datetime/NamedDateTimeFunction.java | 94 ++++++++++++++++++ .../datetime/NamedDateTimeProcessor.java | 98 +++++++++++++++++++ .../function/scalar/datetime/Quarter.java | 94 ++++++++++++++++++ .../scalar/datetime/QuarterProcessor.java | 60 ++++++++++++ .../scalar/datetime/SecondOfMinute.java | 2 +- .../function/scalar/datetime/WeekOfYear.java | 2 +- .../function/scalar/datetime/Year.java | 2 +- .../whitelist/InternalSqlScriptUtils.java | 14 +++ .../xpack/sql/plugin/sql_whitelist.txt | 3 + .../datetime/NamedDateTimeProcessorTests.java | 89 +++++++++++++++++ .../datetime/QuarterProcessorTests.java | 46 +++++++++ .../xpack/qa/sql/cli/ShowTestCase.java | 2 + .../sql/src/main/resources/command.csv-spec | 7 ++ .../sql/src/main/resources/datetime.sql-spec | 55 ++++++++++- .../qa/sql/src/main/resources/docs.csv-spec | 9 +- 30 files changed, 838 insertions(+), 110 deletions(-) create mode 100644 x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/BaseDateTimeFunction.java create mode 100644 x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/BaseDateTimeProcessor.java create mode 100644 x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DayName.java create mode 100644 x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/MonthName.java create mode 100644 x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NamedDateTimeFunction.java create mode 100644 x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NamedDateTimeProcessor.java create mode 100644 x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/Quarter.java create mode 100644 x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/QuarterProcessor.java create mode 100644 x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NamedDateTimeProcessorTests.java create mode 100644 x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/QuarterProcessorTests.java diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/FunctionRegistry.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/FunctionRegistry.java index c9d652861f8..820aafb0116 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/FunctionRegistry.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/FunctionRegistry.java @@ -21,13 +21,16 @@ import org.elasticsearch.xpack.sql.expression.function.aggregate.Sum; import org.elasticsearch.xpack.sql.expression.function.aggregate.SumOfSquares; import org.elasticsearch.xpack.sql.expression.function.aggregate.VarPop; import org.elasticsearch.xpack.sql.expression.function.scalar.arithmetic.Mod; +import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DayName; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DayOfMonth; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DayOfWeek; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DayOfYear; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.HourOfDay; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.MinuteOfDay; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.MinuteOfHour; +import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.MonthName; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.MonthOfYear; +import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.Quarter; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.SecondOfMinute; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.WeekOfYear; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.Year; @@ -62,21 +65,21 @@ import org.elasticsearch.xpack.sql.expression.function.scalar.string.Ascii; import org.elasticsearch.xpack.sql.expression.function.scalar.string.BitLength; import org.elasticsearch.xpack.sql.expression.function.scalar.string.Char; import org.elasticsearch.xpack.sql.expression.function.scalar.string.CharLength; -import org.elasticsearch.xpack.sql.expression.function.scalar.string.LCase; -import org.elasticsearch.xpack.sql.expression.function.scalar.string.LTrim; -import org.elasticsearch.xpack.sql.expression.function.scalar.string.Length; -import org.elasticsearch.xpack.sql.expression.function.scalar.string.RTrim; -import org.elasticsearch.xpack.sql.expression.function.scalar.string.Space; -import org.elasticsearch.xpack.sql.expression.function.scalar.string.UCase; import org.elasticsearch.xpack.sql.expression.function.scalar.string.Concat; import org.elasticsearch.xpack.sql.expression.function.scalar.string.Insert; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.LCase; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.LTrim; import org.elasticsearch.xpack.sql.expression.function.scalar.string.Left; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.Length; import org.elasticsearch.xpack.sql.expression.function.scalar.string.Locate; import org.elasticsearch.xpack.sql.expression.function.scalar.string.Position; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.RTrim; import org.elasticsearch.xpack.sql.expression.function.scalar.string.Repeat; import org.elasticsearch.xpack.sql.expression.function.scalar.string.Replace; import org.elasticsearch.xpack.sql.expression.function.scalar.string.Right; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.Space; import org.elasticsearch.xpack.sql.expression.function.scalar.string.Substring; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.UCase; import org.elasticsearch.xpack.sql.parser.ParsingException; import org.elasticsearch.xpack.sql.tree.Location; import org.elasticsearch.xpack.sql.util.StringUtils; @@ -123,6 +126,9 @@ public class FunctionRegistry { def(MonthOfYear.class, MonthOfYear::new, "MONTH"), def(Year.class, Year::new), def(WeekOfYear.class, WeekOfYear::new, "WEEK"), + def(DayName.class, DayName::new, "DAYNAME"), + def(MonthName.class, MonthName::new, "MONTHNAME"), + def(Quarter.class, Quarter::new), // Math def(Abs.class, Abs::new), def(ACos.class, ACos::new), diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Processors.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Processors.java index 0f36654fa4a..a62aadab467 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Processors.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Processors.java @@ -10,6 +10,8 @@ import org.elasticsearch.common.io.stream.NamedWriteableRegistry.Entry; import org.elasticsearch.xpack.sql.expression.function.scalar.arithmetic.BinaryArithmeticProcessor; import org.elasticsearch.xpack.sql.expression.function.scalar.arithmetic.UnaryArithmeticProcessor; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeProcessor; +import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.NamedDateTimeProcessor; +import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.QuarterProcessor; import org.elasticsearch.xpack.sql.expression.function.scalar.math.BinaryMathProcessor; import org.elasticsearch.xpack.sql.expression.function.scalar.math.MathProcessor; import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.BucketExtractorProcessor; @@ -17,13 +19,13 @@ import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime. import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.ConstantProcessor; import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.HitExtractorProcessor; import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.Processor; -import org.elasticsearch.xpack.sql.expression.function.scalar.string.StringProcessor; import org.elasticsearch.xpack.sql.expression.function.scalar.string.BinaryStringNumericProcessor; import org.elasticsearch.xpack.sql.expression.function.scalar.string.BinaryStringStringProcessor; import org.elasticsearch.xpack.sql.expression.function.scalar.string.ConcatFunctionProcessor; import org.elasticsearch.xpack.sql.expression.function.scalar.string.InsertFunctionProcessor; import org.elasticsearch.xpack.sql.expression.function.scalar.string.LocateFunctionProcessor; import org.elasticsearch.xpack.sql.expression.function.scalar.string.ReplaceFunctionProcessor; +import org.elasticsearch.xpack.sql.expression.function.scalar.string.StringProcessor; import org.elasticsearch.xpack.sql.expression.function.scalar.string.SubstringFunctionProcessor; import java.util.ArrayList; @@ -52,6 +54,8 @@ public final class Processors { entries.add(new Entry(Processor.class, BinaryMathProcessor.NAME, BinaryMathProcessor::new)); // datetime entries.add(new Entry(Processor.class, DateTimeProcessor.NAME, DateTimeProcessor::new)); + entries.add(new Entry(Processor.class, NamedDateTimeProcessor.NAME, NamedDateTimeProcessor::new)); + entries.add(new Entry(Processor.class, QuarterProcessor.NAME, QuarterProcessor::new)); // math entries.add(new Entry(Processor.class, MathProcessor.NAME, MathProcessor::new)); // string diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/BaseDateTimeFunction.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/BaseDateTimeFunction.java new file mode 100644 index 00000000000..2213fad8c8d --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/BaseDateTimeFunction.java @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.sql.expression.function.scalar.datetime; + +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.Expressions; +import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunctionAttribute; +import org.elasticsearch.xpack.sql.expression.function.scalar.UnaryScalarFunction; +import org.elasticsearch.xpack.sql.expression.function.scalar.script.ScriptTemplate; +import org.elasticsearch.xpack.sql.tree.Location; +import org.elasticsearch.xpack.sql.tree.NodeInfo; +import org.elasticsearch.xpack.sql.type.DataType; + +import java.util.TimeZone; + +abstract class BaseDateTimeFunction extends UnaryScalarFunction { + + private final TimeZone timeZone; + private final String name; + + BaseDateTimeFunction(Location location, Expression field, TimeZone timeZone) { + super(location, field); + this.timeZone = timeZone; + + StringBuilder sb = new StringBuilder(super.name()); + // add timezone as last argument + sb.insert(sb.length() - 1, " [" + timeZone.getID() + "]"); + + this.name = sb.toString(); + } + + @Override + protected final NodeInfo info() { + return NodeInfo.create(this, ctorForInfo(), field(), timeZone()); + } + + protected abstract NodeInfo.NodeCtor2 ctorForInfo(); + + @Override + protected TypeResolution resolveType() { + if (field().dataType() == DataType.DATE) { + return TypeResolution.TYPE_RESOLVED; + } + return new TypeResolution("Function [" + functionName() + "] cannot be applied on a non-date expression ([" + + Expressions.name(field()) + "] of type [" + field().dataType().esType + "])"); + } + + public TimeZone timeZone() { + return timeZone; + } + + @Override + public String name() { + return name; + } + + @Override + public boolean foldable() { + return field().foldable(); + } + + @Override + protected ScriptTemplate asScriptFrom(AggregateFunctionAttribute aggregate) { + throw new UnsupportedOperationException(); + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/BaseDateTimeProcessor.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/BaseDateTimeProcessor.java new file mode 100644 index 00000000000..95547ded222 --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/BaseDateTimeProcessor.java @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.sql.expression.function.scalar.datetime; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.Processor; +import org.joda.time.ReadableInstant; + +import java.io.IOException; +import java.util.TimeZone; + +public abstract class BaseDateTimeProcessor implements Processor { + + private final TimeZone timeZone; + + BaseDateTimeProcessor(TimeZone timeZone) { + this.timeZone = timeZone; + } + + BaseDateTimeProcessor(StreamInput in) throws IOException { + timeZone = TimeZone.getTimeZone(in.readString()); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(timeZone.getID()); + } + + TimeZone timeZone() { + return timeZone; + } + + @Override + public Object process(Object l) { + if (l == null) { + return null; + } + long millis; + if (l instanceof String) { + // 6.4+ + millis = Long.parseLong(l.toString()); + } else if (l instanceof ReadableInstant) { + // 6.3- + millis = ((ReadableInstant) l).getMillis(); + } else { + throw new SqlIllegalArgumentException("A string or a date is required; received {}", l); + } + + return doProcess(millis); + } + + abstract Object doProcess(long millis); +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeFunction.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeFunction.java index 60672822278..d87e15084a4 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeFunction.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeFunction.java @@ -6,10 +6,7 @@ package org.elasticsearch.xpack.sql.expression.function.scalar.datetime; import org.elasticsearch.xpack.sql.expression.Expression; -import org.elasticsearch.xpack.sql.expression.Expressions; import org.elasticsearch.xpack.sql.expression.FieldAttribute; -import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunctionAttribute; -import org.elasticsearch.xpack.sql.expression.function.scalar.UnaryScalarFunction; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeProcessor.DateTimeExtractor; import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinitions; @@ -17,7 +14,6 @@ import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definiti import org.elasticsearch.xpack.sql.expression.function.scalar.script.ParamsBuilder; import org.elasticsearch.xpack.sql.expression.function.scalar.script.ScriptTemplate; import org.elasticsearch.xpack.sql.tree.Location; -import org.elasticsearch.xpack.sql.tree.NodeInfo; import org.elasticsearch.xpack.sql.type.DataType; import org.joda.time.DateTime; @@ -31,45 +27,10 @@ import java.util.TimeZone; import static org.elasticsearch.xpack.sql.expression.function.scalar.script.ParamsBuilder.paramsBuilder; import static org.elasticsearch.xpack.sql.expression.function.scalar.script.ScriptTemplate.formatTemplate; -public abstract class DateTimeFunction extends UnaryScalarFunction { - - private final TimeZone timeZone; - private final String name; +public abstract class DateTimeFunction extends BaseDateTimeFunction { DateTimeFunction(Location location, Expression field, TimeZone timeZone) { - super(location, field); - this.timeZone = timeZone; - - StringBuilder sb = new StringBuilder(super.name()); - // add timezone as last argument - sb.insert(sb.length() - 1, " [" + timeZone.getID() + "]"); - - this.name = sb.toString(); - } - - @Override - protected final NodeInfo info() { - return NodeInfo.create(this, ctorForInfo(), field(), timeZone()); - } - - protected abstract NodeInfo.NodeCtor2 ctorForInfo(); - - @Override - protected TypeResolution resolveType() { - if (field().dataType() == DataType.DATE) { - return TypeResolution.TYPE_RESOLVED; - } - return new TypeResolution("Function [" + functionName() + "] cannot be applied on a non-date expression ([" - + Expressions.name(field()) + "] of type [" + field().dataType().esType + "])"); - } - - public TimeZone timeZone() { - return timeZone; - } - - @Override - public boolean foldable() { - return field().foldable(); + super(location, field, timeZone); } @Override @@ -79,7 +40,7 @@ public abstract class DateTimeFunction extends UnaryScalarFunction { return null; } - return dateTimeChrono(folded.getMillis(), timeZone.getID(), chronoField().name()); + return dateTimeChrono(folded.getMillis(), timeZone().getID(), chronoField().name()); } public static Integer dateTimeChrono(long millis, String tzId, String chronoName) { @@ -94,27 +55,21 @@ public abstract class DateTimeFunction extends UnaryScalarFunction { String template = null; template = formatTemplate("{sql}.dateTimeChrono(doc[{}].value.millis, {}, {})"); params.variable(field.name()) - .variable(timeZone.getID()) + .variable(timeZone().getID()) .variable(chronoField().name()); return new ScriptTemplate(template, params.build(), dataType()); } - - @Override - protected ScriptTemplate asScriptFrom(AggregateFunctionAttribute aggregate) { - throw new UnsupportedOperationException(); - } - /** * Used for generating the painless script version of this function when the time zone is not UTC */ protected abstract ChronoField chronoField(); @Override - protected final ProcessorDefinition makeProcessorDefinition() { + protected ProcessorDefinition makeProcessorDefinition() { return new UnaryProcessorDefinition(location(), this, ProcessorDefinitions.toProcessorDefinition(field()), - new DateTimeProcessor(extractor(), timeZone)); + new DateTimeProcessor(extractor(), timeZone())); } protected abstract DateTimeExtractor extractor(); @@ -127,12 +82,6 @@ public abstract class DateTimeFunction extends UnaryScalarFunction { // used for applying ranges public abstract String dateTimeFormat(); - // add tz along the rest of the params - @Override - public String name() { - return name; - } - @Override public boolean equals(Object obj) { if (obj == null || obj.getClass() != getClass()) { @@ -140,11 +89,11 @@ public abstract class DateTimeFunction extends UnaryScalarFunction { } DateTimeFunction other = (DateTimeFunction) obj; return Objects.equals(other.field(), field()) - && Objects.equals(other.timeZone, timeZone); + && Objects.equals(other.timeZone(), timeZone()); } @Override public int hashCode() { - return Objects.hash(field(), timeZone); + return Objects.hash(field(), timeZone()); } } \ No newline at end of file diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeProcessor.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeProcessor.java index d135b8a0865..d34b1c1e390 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeProcessor.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeProcessor.java @@ -7,19 +7,16 @@ package org.elasticsearch.xpack.sql.expression.function.scalar.datetime; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; -import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.Processor; import org.joda.time.DateTime; import org.joda.time.DateTimeFieldType; import org.joda.time.DateTimeZone; import org.joda.time.ReadableDateTime; -import org.joda.time.ReadableInstant; import java.io.IOException; import java.util.Objects; import java.util.TimeZone; -public class DateTimeProcessor implements Processor { +public class DateTimeProcessor extends BaseDateTimeProcessor { public enum DateTimeExtractor { DAY_OF_MONTH(DateTimeFieldType.dayOfMonth()), @@ -45,24 +42,22 @@ public class DateTimeProcessor implements Processor { } public static final String NAME = "dt"; - private final DateTimeExtractor extractor; - private final TimeZone timeZone; public DateTimeProcessor(DateTimeExtractor extractor, TimeZone timeZone) { + super(timeZone); this.extractor = extractor; - this.timeZone = timeZone; } public DateTimeProcessor(StreamInput in) throws IOException { + super(in); extractor = in.readEnum(DateTimeExtractor.class); - timeZone = TimeZone.getTimeZone(in.readString()); } @Override public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); out.writeEnum(extractor); - out.writeString(timeZone.getID()); } @Override @@ -75,32 +70,15 @@ public class DateTimeProcessor implements Processor { } @Override - public Object process(Object l) { - if (l == null) { - return null; - } - - ReadableDateTime dt; - if (l instanceof String) { - // 6.4+ - final long millis = Long.parseLong(l.toString()); - dt = new DateTime(millis, DateTimeZone.forTimeZone(timeZone)); - } else if (l instanceof ReadableInstant) { - // 6.3- - dt = (ReadableDateTime) l; - if (!TimeZone.getTimeZone("UTC").equals(timeZone)) { - dt = dt.toDateTime().withZone(DateTimeZone.forTimeZone(timeZone)); - } - } else { - throw new SqlIllegalArgumentException("A string or a date is required; received {}", l); - } + public Object doProcess(long millis) { + ReadableDateTime dt = new DateTime(millis, DateTimeZone.forTimeZone(timeZone())); return extractor.extract(dt); } @Override public int hashCode() { - return Objects.hash(extractor, timeZone); + return Objects.hash(extractor, timeZone()); } @Override @@ -110,7 +88,7 @@ public class DateTimeProcessor implements Processor { } DateTimeProcessor other = (DateTimeProcessor) obj; return Objects.equals(extractor, other.extractor) - && Objects.equals(timeZone, other.timeZone); + && Objects.equals(timeZone(), other.timeZone()); } @Override diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DayName.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DayName.java new file mode 100644 index 00000000000..2f5ba7eeaca --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DayName.java @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.expression.function.scalar.datetime; + +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.NamedDateTimeProcessor.NameExtractor; +import org.elasticsearch.xpack.sql.tree.Location; +import org.elasticsearch.xpack.sql.tree.NodeInfo.NodeCtor2; + +import java.util.TimeZone; + +/** + * Extract the day of the week from a datetime in text format (Monday, Tuesday etc.) + */ +public class DayName extends NamedDateTimeFunction { + protected static final String DAY_NAME_FORMAT = "EEEE"; + + public DayName(Location location, Expression field, TimeZone timeZone) { + super(location, field, timeZone); + } + + @Override + protected NodeCtor2 ctorForInfo() { + return DayName::new; + } + + @Override + protected DayName replaceChild(Expression newChild) { + return new DayName(location(), newChild, timeZone()); + } + + @Override + protected String dateTimeFormat() { + return DAY_NAME_FORMAT; + } + + @Override + protected NameExtractor nameExtractor() { + return NameExtractor.DAY_NAME; + } + + @Override + public String extractName(long millis, String tzId) { + return nameExtractor().extract(millis, tzId); + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DayOfMonth.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DayOfMonth.java index 1ac3771d49d..ebb576b4648 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DayOfMonth.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DayOfMonth.java @@ -22,7 +22,7 @@ public class DayOfMonth extends DateTimeFunction { } @Override - protected NodeCtor2 ctorForInfo() { + protected NodeCtor2 ctorForInfo() { return DayOfMonth::new; } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DayOfWeek.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DayOfWeek.java index 7582ece6250..d840d4d71df 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DayOfWeek.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DayOfWeek.java @@ -22,7 +22,7 @@ public class DayOfWeek extends DateTimeFunction { } @Override - protected NodeCtor2 ctorForInfo() { + protected NodeCtor2 ctorForInfo() { return DayOfWeek::new; } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DayOfYear.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DayOfYear.java index 8f5e0618832..1fa248d9c20 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DayOfYear.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DayOfYear.java @@ -23,7 +23,7 @@ public class DayOfYear extends DateTimeFunction { } @Override - protected NodeCtor2 ctorForInfo() { + protected NodeCtor2 ctorForInfo() { return DayOfYear::new; } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/HourOfDay.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/HourOfDay.java index 5a2bc681ab8..4df28bddad0 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/HourOfDay.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/HourOfDay.java @@ -22,7 +22,7 @@ public class HourOfDay extends DateTimeFunction { } @Override - protected NodeCtor2 ctorForInfo() { + protected NodeCtor2 ctorForInfo() { return HourOfDay::new; } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/MinuteOfDay.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/MinuteOfDay.java index 2840fa0c21b..ef0fb0bce18 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/MinuteOfDay.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/MinuteOfDay.java @@ -23,7 +23,7 @@ public class MinuteOfDay extends DateTimeFunction { } @Override - protected NodeCtor2 ctorForInfo() { + protected NodeCtor2 ctorForInfo() { return MinuteOfDay::new; } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/MinuteOfHour.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/MinuteOfHour.java index d577bb91696..f5ab095ef24 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/MinuteOfHour.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/MinuteOfHour.java @@ -22,7 +22,7 @@ public class MinuteOfHour extends DateTimeFunction { } @Override - protected NodeCtor2 ctorForInfo() { + protected NodeCtor2 ctorForInfo() { return MinuteOfHour::new; } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/MonthName.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/MonthName.java new file mode 100644 index 00000000000..170c80c10f9 --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/MonthName.java @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.expression.function.scalar.datetime; + +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.NamedDateTimeProcessor.NameExtractor; +import org.elasticsearch.xpack.sql.tree.Location; +import org.elasticsearch.xpack.sql.tree.NodeInfo.NodeCtor2; + +import java.util.TimeZone; + +/** + * Extract the month from a datetime in text format (January, February etc.) + */ +public class MonthName extends NamedDateTimeFunction { + protected static final String MONTH_NAME_FORMAT = "MMMM"; + + public MonthName(Location location, Expression field, TimeZone timeZone) { + super(location, field, timeZone); + } + + @Override + protected NodeCtor2 ctorForInfo() { + return MonthName::new; + } + + @Override + protected MonthName replaceChild(Expression newChild) { + return new MonthName(location(), newChild, timeZone()); + } + + @Override + protected String dateTimeFormat() { + return MONTH_NAME_FORMAT; + } + + @Override + public String extractName(long millis, String tzId) { + return nameExtractor().extract(millis, tzId); + } + + @Override + protected NameExtractor nameExtractor() { + return NameExtractor.MONTH_NAME; + } + +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/MonthOfYear.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/MonthOfYear.java index 3a2d51bee78..503a771611e 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/MonthOfYear.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/MonthOfYear.java @@ -22,7 +22,7 @@ public class MonthOfYear extends DateTimeFunction { } @Override - protected NodeCtor2 ctorForInfo() { + protected NodeCtor2 ctorForInfo() { return MonthOfYear::new; } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NamedDateTimeFunction.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NamedDateTimeFunction.java new file mode 100644 index 00000000000..c3e10981ce1 --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NamedDateTimeFunction.java @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.expression.function.scalar.datetime; + +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.FieldAttribute; +import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.NamedDateTimeProcessor.NameExtractor; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinitions; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.UnaryProcessorDefinition; +import org.elasticsearch.xpack.sql.expression.function.scalar.script.ParamsBuilder; +import org.elasticsearch.xpack.sql.expression.function.scalar.script.ScriptTemplate; +import org.elasticsearch.xpack.sql.tree.Location; +import org.elasticsearch.xpack.sql.type.DataType; +import org.elasticsearch.xpack.sql.util.StringUtils; +import org.joda.time.DateTime; + +import java.util.Objects; +import java.util.TimeZone; + +import static org.elasticsearch.xpack.sql.expression.function.scalar.script.ParamsBuilder.paramsBuilder; +import static org.elasticsearch.xpack.sql.expression.function.scalar.script.ScriptTemplate.formatTemplate; + +/* + * Base class for "naming" date/time functions like month_name and day_name + */ +abstract class NamedDateTimeFunction extends BaseDateTimeFunction { + + NamedDateTimeFunction(Location location, Expression field, TimeZone timeZone) { + super(location, field, timeZone); + } + + @Override + public Object fold() { + DateTime folded = (DateTime) field().fold(); + if (folded == null) { + return null; + } + + return extractName(folded.getMillis(), timeZone().getID()); + } + + public abstract String extractName(long millis, String tzId); + + @Override + protected ScriptTemplate asScriptFrom(FieldAttribute field) { + ParamsBuilder params = paramsBuilder(); + + String template = null; + template = formatTemplate(formatMethodName("{sql}.{method_name}(doc[{}].value.millis, {})")); + params.variable(field.name()) + .variable(timeZone().getID()); + + return new ScriptTemplate(template, params.build(), dataType()); + } + + private String formatMethodName(String template) { + // the Painless method name will be the enum's lower camelcase name + return template.replace("{method_name}", StringUtils.underscoreToLowerCamelCase(nameExtractor().toString())); + } + + @Override + protected final ProcessorDefinition makeProcessorDefinition() { + return new UnaryProcessorDefinition(location(), this, ProcessorDefinitions.toProcessorDefinition(field()), + new NamedDateTimeProcessor(nameExtractor(), timeZone())); + } + + protected abstract NameExtractor nameExtractor(); + + protected abstract String dateTimeFormat(); + + @Override + public DataType dataType() { + return DataType.KEYWORD; + } + + @Override + public boolean equals(Object obj) { + if (obj == null || obj.getClass() != getClass()) { + return false; + } + NamedDateTimeFunction other = (NamedDateTimeFunction) obj; + return Objects.equals(other.field(), field()) + && Objects.equals(other.timeZone(), timeZone()); + } + + @Override + public int hashCode() { + return Objects.hash(field(), timeZone()); + } +} \ No newline at end of file diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NamedDateTimeProcessor.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NamedDateTimeProcessor.java new file mode 100644 index 00000000000..478ad8ee09f --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NamedDateTimeProcessor.java @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.expression.function.scalar.datetime; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; +import java.util.Objects; +import java.util.TimeZone; +import java.util.function.BiFunction; + +public class NamedDateTimeProcessor extends BaseDateTimeProcessor { + + public enum NameExtractor { + // for the moment we'll use no specific Locale, but we might consider introducing a Locale parameter, just like the timeZone one + DAY_NAME((Long millis, String tzId) -> { + ZonedDateTime time = ZonedDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneId.of(tzId)); + return time.format(DateTimeFormatter.ofPattern(DayName.DAY_NAME_FORMAT, Locale.ROOT)); + }), + MONTH_NAME((Long millis, String tzId) -> { + ZonedDateTime time = ZonedDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneId.of(tzId)); + return time.format(DateTimeFormatter.ofPattern(MonthName.MONTH_NAME_FORMAT, Locale.ROOT)); + }); + + private final BiFunction apply; + + NameExtractor(BiFunction apply) { + this.apply = apply; + } + + public final String extract(Long millis, String tzId) { + return apply.apply(millis, tzId); + } + } + + public static final String NAME = "ndt"; + + private final NameExtractor extractor; + + public NamedDateTimeProcessor(NameExtractor extractor, TimeZone timeZone) { + super(timeZone); + this.extractor = extractor; + } + + public NamedDateTimeProcessor(StreamInput in) throws IOException { + super(in); + extractor = in.readEnum(NameExtractor.class); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeEnum(extractor); + } + + @Override + public String getWriteableName() { + return NAME; + } + + NameExtractor extractor() { + return extractor; + } + + @Override + public Object doProcess(long millis) { + return extractor.extract(millis, timeZone().getID()); + } + + @Override + public int hashCode() { + return Objects.hash(extractor, timeZone()); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || obj.getClass() != getClass()) { + return false; + } + NamedDateTimeProcessor other = (NamedDateTimeProcessor) obj; + return Objects.equals(extractor, other.extractor) + && Objects.equals(timeZone(), other.timeZone()); + } + + @Override + public String toString() { + return extractor.toString(); + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/Quarter.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/Quarter.java new file mode 100644 index 00000000000..22e368b0ec6 --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/Quarter.java @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.sql.expression.function.scalar.datetime; + +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.FieldAttribute; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinitions; +import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.UnaryProcessorDefinition; +import org.elasticsearch.xpack.sql.expression.function.scalar.script.ParamsBuilder; +import org.elasticsearch.xpack.sql.expression.function.scalar.script.ScriptTemplate; +import org.elasticsearch.xpack.sql.tree.Location; +import org.elasticsearch.xpack.sql.tree.NodeInfo.NodeCtor2; +import org.elasticsearch.xpack.sql.type.DataType; +import org.joda.time.DateTime; + +import java.util.Objects; +import java.util.TimeZone; + +import static org.elasticsearch.xpack.sql.expression.function.scalar.datetime.QuarterProcessor.quarter; +import static org.elasticsearch.xpack.sql.expression.function.scalar.script.ParamsBuilder.paramsBuilder; +import static org.elasticsearch.xpack.sql.expression.function.scalar.script.ScriptTemplate.formatTemplate; + +public class Quarter extends BaseDateTimeFunction { + + protected static final String QUARTER_FORMAT = "q"; + + public Quarter(Location location, Expression field, TimeZone timeZone) { + super(location, field, timeZone); + } + + @Override + public Object fold() { + DateTime folded = (DateTime) field().fold(); + if (folded == null) { + return null; + } + + return quarter(folded.getMillis(), timeZone().getID()); + } + + @Override + protected ScriptTemplate asScriptFrom(FieldAttribute field) { + ParamsBuilder params = paramsBuilder(); + + String template = null; + template = formatTemplate("{sql}.quarter(doc[{}].value.millis, {})"); + params.variable(field.name()) + .variable(timeZone().getID()); + + return new ScriptTemplate(template, params.build(), dataType()); + } + + @Override + protected NodeCtor2 ctorForInfo() { + return Quarter::new; + } + + @Override + protected Quarter replaceChild(Expression newChild) { + return new Quarter(location(), newChild, timeZone()); + } + + @Override + protected ProcessorDefinition makeProcessorDefinition() { + return new UnaryProcessorDefinition(location(), this, ProcessorDefinitions.toProcessorDefinition(field()), + new QuarterProcessor(timeZone())); + } + + @Override + public DataType dataType() { + return DataType.INTEGER; + } + + @Override + public boolean equals(Object obj) { + if (obj == null || obj.getClass() != getClass()) { + return false; + } + BaseDateTimeFunction other = (BaseDateTimeFunction) obj; + return Objects.equals(other.field(), field()) + && Objects.equals(other.timeZone(), timeZone()); + } + + @Override + public int hashCode() { + return Objects.hash(field(), timeZone()); + } + +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/QuarterProcessor.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/QuarterProcessor.java new file mode 100644 index 00000000000..c6904216d0f --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/QuarterProcessor.java @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.sql.expression.function.scalar.datetime; + +import org.elasticsearch.common.io.stream.StreamInput; + +import java.io.IOException; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; +import java.util.Objects; +import java.util.TimeZone; + +public class QuarterProcessor extends BaseDateTimeProcessor { + + public QuarterProcessor(TimeZone timeZone) { + super(timeZone); + } + + public QuarterProcessor(StreamInput in) throws IOException { + super(in); + } + + public static final String NAME = "q"; + + @Override + public String getWriteableName() { + return NAME; + } + + @Override + public Object doProcess(long millis) { + return quarter(millis, timeZone().getID()); + } + + public static Integer quarter(long millis, String tzId) { + ZonedDateTime time = ZonedDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneId.of(tzId)); + return Integer.valueOf(time.format(DateTimeFormatter.ofPattern(Quarter.QUARTER_FORMAT, Locale.ROOT))); + } + + @Override + public int hashCode() { + return Objects.hash(timeZone()); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || obj.getClass() != getClass()) { + return false; + } + DateTimeProcessor other = (DateTimeProcessor) obj; + return Objects.equals(timeZone(), other.timeZone()); + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/SecondOfMinute.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/SecondOfMinute.java index 883502c017d..3522eb10ffe 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/SecondOfMinute.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/SecondOfMinute.java @@ -22,7 +22,7 @@ public class SecondOfMinute extends DateTimeFunction { } @Override - protected NodeCtor2 ctorForInfo() { + protected NodeCtor2 ctorForInfo() { return SecondOfMinute::new; } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/WeekOfYear.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/WeekOfYear.java index eef2c48ad0f..59948165f71 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/WeekOfYear.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/WeekOfYear.java @@ -22,7 +22,7 @@ public class WeekOfYear extends DateTimeFunction { } @Override - protected NodeCtor2 ctorForInfo() { + protected NodeCtor2 ctorForInfo() { return WeekOfYear::new; } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/Year.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/Year.java index 28d475e4c70..2b065329be3 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/Year.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/Year.java @@ -22,7 +22,7 @@ public class Year extends DateTimeHistogramFunction { } @Override - protected NodeCtor2 ctorForInfo() { + protected NodeCtor2 ctorForInfo() { return Year::new; } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/whitelist/InternalSqlScriptUtils.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/whitelist/InternalSqlScriptUtils.java index 12faeb78b66..f0a79f15e36 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/whitelist/InternalSqlScriptUtils.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/whitelist/InternalSqlScriptUtils.java @@ -6,6 +6,8 @@ package org.elasticsearch.xpack.sql.expression.function.scalar.whitelist; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeFunction; +import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.NamedDateTimeProcessor.NameExtractor; +import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.QuarterProcessor; import org.elasticsearch.xpack.sql.expression.function.scalar.string.BinaryStringNumericProcessor.BinaryStringNumericOperation; import org.elasticsearch.xpack.sql.expression.function.scalar.string.BinaryStringStringProcessor.BinaryStringStringOperation; import org.elasticsearch.xpack.sql.expression.function.scalar.string.ConcatFunctionProcessor; @@ -28,6 +30,18 @@ public final class InternalSqlScriptUtils { return DateTimeFunction.dateTimeChrono(millis, tzId, chronoName); } + public static String dayName(long millis, String tzId) { + return NameExtractor.DAY_NAME.extract(millis, tzId); + } + + public static String monthName(long millis, String tzId) { + return NameExtractor.MONTH_NAME.extract(millis, tzId); + } + + public static Integer quarter(long millis, String tzId) { + return QuarterProcessor.quarter(millis, tzId); + } + public static Integer ascii(String s) { return (Integer) StringOperation.ASCII.apply(s); } diff --git a/x-pack/plugin/sql/src/main/resources/org/elasticsearch/xpack/sql/plugin/sql_whitelist.txt b/x-pack/plugin/sql/src/main/resources/org/elasticsearch/xpack/sql/plugin/sql_whitelist.txt index 8f86685889c..0f12d32d44e 100644 --- a/x-pack/plugin/sql/src/main/resources/org/elasticsearch/xpack/sql/plugin/sql_whitelist.txt +++ b/x-pack/plugin/sql/src/main/resources/org/elasticsearch/xpack/sql/plugin/sql_whitelist.txt @@ -9,6 +9,9 @@ class org.elasticsearch.xpack.sql.expression.function.scalar.whitelist.InternalSqlScriptUtils { Integer dateTimeChrono(long, String, String) + String dayName(long, String) + String monthName(long, String) + Integer quarter(long, String) Integer ascii(String) Integer bitLength(String) String character(Number) diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NamedDateTimeProcessorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NamedDateTimeProcessorTests.java new file mode 100644 index 00000000000..3d57675e209 --- /dev/null +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NamedDateTimeProcessorTests.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.sql.expression.function.scalar.datetime; + +import org.elasticsearch.common.io.stream.Writeable.Reader; +import org.elasticsearch.test.AbstractWireSerializingTestCase; +import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.NamedDateTimeProcessor.NameExtractor; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; + +import java.io.IOException; +import java.util.TimeZone; + +public class NamedDateTimeProcessorTests extends AbstractWireSerializingTestCase { + private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); + + public static NamedDateTimeProcessor randomNamedDateTimeProcessor() { + return new NamedDateTimeProcessor(randomFrom(NameExtractor.values()), UTC); + } + + @Override + protected NamedDateTimeProcessor createTestInstance() { + return randomNamedDateTimeProcessor(); + } + + @Override + protected Reader instanceReader() { + return NamedDateTimeProcessor::new; + } + + @Override + protected NamedDateTimeProcessor mutateInstance(NamedDateTimeProcessor instance) throws IOException { + NameExtractor replaced = randomValueOtherThan(instance.extractor(), () -> randomFrom(NameExtractor.values())); + return new NamedDateTimeProcessor(replaced, UTC); + } + + public void testValidDayNamesInUTC() { + NamedDateTimeProcessor proc = new NamedDateTimeProcessor(NameExtractor.DAY_NAME, UTC); + assertEquals("Thursday", proc.process("0")); + assertEquals("Saturday", proc.process("-64164233612338")); + assertEquals("Monday", proc.process("64164233612338")); + + assertEquals("Thursday", proc.process(new DateTime(0L, DateTimeZone.UTC))); + assertEquals("Thursday", proc.process(new DateTime(-5400, 12, 25, 2, 0, DateTimeZone.UTC))); + assertEquals("Friday", proc.process(new DateTime(30, 2, 1, 12, 13, DateTimeZone.UTC))); + assertEquals("Tuesday", proc.process(new DateTime(10902, 8, 22, 11, 11, DateTimeZone.UTC))); + } + + public void testValidDayNamesWithNonUTCTimeZone() { + NamedDateTimeProcessor proc = new NamedDateTimeProcessor(NameExtractor.DAY_NAME, TimeZone.getTimeZone("GMT-10:00")); + assertEquals("Wednesday", proc.process("0")); + assertEquals("Friday", proc.process("-64164233612338")); + assertEquals("Monday", proc.process("64164233612338")); + + assertEquals("Wednesday", proc.process(new DateTime(0L, DateTimeZone.UTC))); + assertEquals("Wednesday", proc.process(new DateTime(-5400, 12, 25, 2, 0, DateTimeZone.UTC))); + assertEquals("Friday", proc.process(new DateTime(30, 2, 1, 12, 13, DateTimeZone.UTC))); + assertEquals("Tuesday", proc.process(new DateTime(10902, 8, 22, 11, 11, DateTimeZone.UTC))); + assertEquals("Monday", proc.process(new DateTime(10902, 8, 22, 9, 59, DateTimeZone.UTC))); + } + + public void testValidMonthNamesInUTC() { + NamedDateTimeProcessor proc = new NamedDateTimeProcessor(NameExtractor.MONTH_NAME, UTC); + assertEquals("January", proc.process("0")); + assertEquals("September", proc.process("-64164233612338")); + assertEquals("April", proc.process("64164233612338")); + + assertEquals("January", proc.process(new DateTime(0L, DateTimeZone.UTC))); + assertEquals("December", proc.process(new DateTime(-5400, 12, 25, 10, 10, DateTimeZone.UTC))); + assertEquals("February", proc.process(new DateTime(30, 2, 1, 12, 13, DateTimeZone.UTC))); + assertEquals("August", proc.process(new DateTime(10902, 8, 22, 11, 11, DateTimeZone.UTC))); + } + + public void testValidMonthNamesWithNonUTCTimeZone() { + NamedDateTimeProcessor proc = new NamedDateTimeProcessor(NameExtractor.MONTH_NAME, TimeZone.getTimeZone("GMT-3:00")); + assertEquals("December", proc.process("0")); + assertEquals("August", proc.process("-64165813612338")); // GMT: Tuesday, September 1, -0064 2:53:07.662 AM + assertEquals("April", proc.process("64164233612338")); // GMT: Monday, April 14, 4003 2:13:32.338 PM + + assertEquals("December", proc.process(new DateTime(0L, DateTimeZone.UTC))); + assertEquals("November", proc.process(new DateTime(-5400, 12, 1, 1, 1, DateTimeZone.UTC))); + assertEquals("February", proc.process(new DateTime(30, 2, 1, 12, 13, DateTimeZone.UTC))); + assertEquals("July", proc.process(new DateTime(10902, 8, 1, 2, 59, DateTimeZone.UTC))); + assertEquals("August", proc.process(new DateTime(10902, 8, 1, 3, 00, DateTimeZone.UTC))); + } +} diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/QuarterProcessorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/QuarterProcessorTests.java new file mode 100644 index 00000000000..7747bb8cae4 --- /dev/null +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/QuarterProcessorTests.java @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.sql.expression.function.scalar.datetime; + +import org.elasticsearch.test.ESTestCase; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; + +import java.util.TimeZone; + +public class QuarterProcessorTests extends ESTestCase { + + private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); + + public void testQuarterWithUTCTimezone() { + QuarterProcessor proc = new QuarterProcessor(UTC); + + assertEquals(1, proc.process(new DateTime(0L, DateTimeZone.UTC))); + assertEquals(4, proc.process(new DateTime(-5400, 12, 25, 10, 10, DateTimeZone.UTC))); + assertEquals(1, proc.process(new DateTime(30, 2, 1, 12, 13, DateTimeZone.UTC))); + assertEquals(3, proc.process(new DateTime(10902, 8, 22, 11, 11, DateTimeZone.UTC))); + + assertEquals(1, proc.process("0")); + assertEquals(3, proc.process("-64164233612338")); + assertEquals(2, proc.process("64164233612338")); + } + + public void testValidDayNamesWithNonUTCTimeZone() { + QuarterProcessor proc = new QuarterProcessor(TimeZone.getTimeZone("GMT-10:00")); + assertEquals(4, proc.process(new DateTime(0L, DateTimeZone.UTC))); + assertEquals(4, proc.process(new DateTime(-5400, 1, 1, 5, 0, DateTimeZone.UTC))); + assertEquals(1, proc.process(new DateTime(30, 4, 1, 9, 59, DateTimeZone.UTC))); + + proc = new QuarterProcessor(TimeZone.getTimeZone("GMT+10:00")); + assertEquals(4, proc.process(new DateTime(10902, 9, 30, 14, 1, DateTimeZone.UTC))); + assertEquals(3, proc.process(new DateTime(10902, 9, 30, 13, 59, DateTimeZone.UTC))); + + assertEquals(1, proc.process("0")); + assertEquals(3, proc.process("-64164233612338")); + assertEquals(2, proc.process("64164233612338")); + } +} diff --git a/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/ShowTestCase.java b/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/ShowTestCase.java index f5b9381c54b..601dca8abd4 100644 --- a/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/ShowTestCase.java +++ b/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/ShowTestCase.java @@ -65,6 +65,8 @@ public abstract class ShowTestCase extends CliIntegrationTestCase { assertThat(readLine(), RegexMatcher.matches("\\s*DAY_OF_YEAR\\s*\\|\\s*SCALAR\\s*")); assertThat(readLine(), RegexMatcher.matches("\\s*HOUR_OF_DAY\\s*\\|\\s*SCALAR\\s*")); assertThat(readLine(), RegexMatcher.matches("\\s*MINUTE_OF_DAY\\s*\\|\\s*SCALAR\\s*")); + assertThat(readLine(), RegexMatcher.matches("\\s*DAY_NAME\\s*\\|\\s*SCALAR\\s*")); + assertThat(readLine(), RegexMatcher.matches("\\s*DAYNAME\\s*\\|\\s*SCALAR\\s*")); assertEquals("", readLine()); } } diff --git a/x-pack/qa/sql/src/main/resources/command.csv-spec b/x-pack/qa/sql/src/main/resources/command.csv-spec index 77d397fa2b5..28aadeded2c 100644 --- a/x-pack/qa/sql/src/main/resources/command.csv-spec +++ b/x-pack/qa/sql/src/main/resources/command.csv-spec @@ -38,6 +38,11 @@ MONTH |SCALAR YEAR |SCALAR WEEK_OF_YEAR |SCALAR WEEK |SCALAR +DAY_NAME |SCALAR +DAYNAME |SCALAR +MONTH_NAME |SCALAR +MONTHNAME |SCALAR +QUARTER |SCALAR ABS |SCALAR ACOS |SCALAR ASIN |SCALAR @@ -130,6 +135,8 @@ DAY_OF_WEEK |SCALAR DAY_OF_YEAR |SCALAR HOUR_OF_DAY |SCALAR MINUTE_OF_DAY |SCALAR +DAY_NAME |SCALAR +DAYNAME |SCALAR ; showTables diff --git a/x-pack/qa/sql/src/main/resources/datetime.sql-spec b/x-pack/qa/sql/src/main/resources/datetime.sql-spec index 20ea8329c8f..81012b7bebf 100644 --- a/x-pack/qa/sql/src/main/resources/datetime.sql-spec +++ b/x-pack/qa/sql/src/main/resources/datetime.sql-spec @@ -12,34 +12,83 @@ dateTimeDay SELECT DAY(birth_date) d, last_name l FROM "test_emp" WHERE emp_no < 10010 ORDER BY emp_no; + dateTimeDayOfMonth SELECT DAY_OF_MONTH(birth_date) d, last_name l FROM "test_emp" WHERE emp_no < 10010 ORDER BY emp_no; + dateTimeMonth SELECT MONTH(birth_date) d, last_name l FROM "test_emp" WHERE emp_no < 10010 ORDER BY emp_no; + dateTimeYear SELECT YEAR(birth_date) d, last_name l FROM "test_emp" WHERE emp_no < 10010 ORDER BY emp_no; +monthNameFromStringDate +SELECT MONTHNAME(CAST('2018-09-03' AS TIMESTAMP)) month FROM "test_emp" limit 1; + +dayNameFromStringDate +SELECT DAYNAME(CAST('2018-09-03' AS TIMESTAMP)) day FROM "test_emp" limit 1; + +quarterSelect +SELECT QUARTER(hire_date) q, hire_date FROM test_emp ORDER BY hire_date LIMIT 15; + // // Filter // + dateTimeFilterDayOfMonth SELECT DAY_OF_MONTH(birth_date) AS d, last_name l FROM "test_emp" WHERE DAY_OF_MONTH(birth_date) <= 10 ORDER BY emp_no LIMIT 5; + dateTimeFilterMonth SELECT MONTH(birth_date) AS d, last_name l FROM "test_emp" WHERE MONTH(birth_date) <= 5 ORDER BY emp_no LIMIT 5; + dateTimeFilterYear SELECT YEAR(birth_date) AS d, last_name l FROM "test_emp" WHERE YEAR(birth_date) <= 1960 ORDER BY emp_no LIMIT 5; +monthNameFilterWithFirstLetter +SELECT MONTHNAME(hire_date) AS m, hire_date FROM "test_emp" WHERE LEFT(MONTHNAME(hire_date), 1) = 'J' ORDER BY hire_date LIMIT 10; + +monthNameFilterWithFullName +SELECT MONTHNAME(hire_date) AS m, hire_date FROM "test_emp" WHERE MONTHNAME(hire_date) = 'August' ORDER BY hire_date LIMIT 10; + +dayNameFilterWithFullName +SELECT DAYNAME(hire_date) AS d, hire_date FROM "test_emp" WHERE DAYNAME(hire_date) = 'Sunday' ORDER BY hire_date LIMIT 10; + +dayNameAndMonthNameAsFilter +SELECT first_name, last_name FROM "test_emp" WHERE DAYNAME(hire_date) = 'Sunday' AND MONTHNAME(hire_date) = 'January' ORDER BY hire_date LIMIT 10; + +quarterWithFilter +SELECT QUARTER(hire_date) quarter, hire_date FROM test_emp WHERE QUARTER(hire_date) > 2 ORDER BY hire_date LIMIT 15; // // Aggregate // - dateTimeAggByYear SELECT YEAR(birth_date) AS d, CAST(SUM(emp_no) AS INT) s FROM "test_emp" GROUP BY YEAR(birth_date) ORDER BY YEAR(birth_date) LIMIT 13; -dateTimeAggByMonth +dateTimeAggByMonthWithOrderBy SELECT MONTH(birth_date) AS d, COUNT(*) AS c, CAST(SUM(emp_no) AS INT) s FROM "test_emp" GROUP BY MONTH(birth_date) ORDER BY MONTH(birth_date) DESC; -dateTimeAggByDayOfMonth +dateTimeAggByDayOfMonthWithOrderBy SELECT DAY_OF_MONTH(birth_date) AS d, COUNT(*) AS c, CAST(SUM(emp_no) AS INT) s FROM "test_emp" GROUP BY DAY_OF_MONTH(birth_date) ORDER BY DAY_OF_MONTH(birth_date) DESC; + +monthNameWithGroupBy +SELECT MONTHNAME("hire_date") AS month, COUNT(*) AS count FROM "test_emp" GROUP BY MONTHNAME("hire_date"), MONTH("hire_date") ORDER BY MONTH("hire_date"); + +monthNameWithDoubleGroupByAndOrderBy +SELECT MONTHNAME("hire_date") AS month, COUNT(*) AS count FROM "test_emp" GROUP BY MONTHNAME("hire_date"), MONTH("hire_date") ORDER BY MONTHNAME("hire_date") DESC; + +// AwaitsFix https://github.com/elastic/elasticsearch/issues/33519 +// monthNameWithGroupByOrderByAndHaving +// SELECT CAST(MAX("salary") AS DOUBLE) max_salary, MONTHNAME("hire_date") month_name FROM "test_emp" GROUP BY MONTHNAME("hire_date") HAVING MAX("salary") > 50000 ORDER BY MONTHNAME(hire_date); +// dayNameWithHaving +// SELECT DAYNAME("hire_date") FROM "test_emp" GROUP BY DAYNAME("hire_date") HAVING MAX("emp_no") > ASCII(DAYNAME("hire_date")); + +dayNameWithDoubleGroupByAndOrderBy +SELECT COUNT(*) c, DAYNAME(hire_date) day_name, DAY(hire_date) day FROM test_emp WHERE MONTHNAME(hire_date) = 'August' GROUP BY DAYNAME(hire_date), DAY(hire_date) ORDER BY DAYNAME(hire_date), DAY(hire_date); + +dayNameWithGroupByOrderByAndHaving +SELECT CAST(MAX(salary) AS DOUBLE) max_salary, DAYNAME(hire_date) day_name FROM test_emp GROUP BY DAYNAME(hire_date) HAVING MAX(salary) > 50000 ORDER BY DAYNAME("hire_date"); + +quarterWithGroupByAndOrderBy +SELECT QUARTER(hire_date) quarter, COUNT(*) hires FROM test_emp GROUP BY QUARTER(hire_date) ORDER BY QUARTER(hire_date); \ No newline at end of file diff --git a/x-pack/qa/sql/src/main/resources/docs.csv-spec b/x-pack/qa/sql/src/main/resources/docs.csv-spec index 2a4f29fcf5d..52356bdfd52 100644 --- a/x-pack/qa/sql/src/main/resources/docs.csv-spec +++ b/x-pack/qa/sql/src/main/resources/docs.csv-spec @@ -214,6 +214,11 @@ MONTH |SCALAR YEAR |SCALAR WEEK_OF_YEAR |SCALAR WEEK |SCALAR +DAY_NAME |SCALAR +DAYNAME |SCALAR +MONTH_NAME |SCALAR +MONTHNAME |SCALAR +QUARTER |SCALAR ABS |SCALAR ACOS |SCALAR ASIN |SCALAR @@ -318,7 +323,9 @@ DAY |SCALAR DAY_OF_WEEK |SCALAR DAY_OF_YEAR |SCALAR HOUR_OF_DAY |SCALAR -MINUTE_OF_DAY |SCALAR +MINUTE_OF_DAY |SCALAR +DAY_NAME |SCALAR +DAYNAME |SCALAR // end::showFunctionsWithPattern ; From 2f3b542d57a6a1e50ba67e4c69b9dece0cac180b Mon Sep 17 00:00:00 2001 From: Ed Savage <32410745+edsavage@users.noreply.github.com> Date: Tue, 11 Sep 2018 12:48:14 +0100 Subject: [PATCH 05/78] HLRC: Add ML get categories API (#33465) HLRC: Adding the ML 'get categories' API --- .../client/MLRequestConverters.java | 15 ++ .../client/MachineLearningClient.java | 41 +++++ .../client/ml/GetCategoriesRequest.java | 128 ++++++++++++++++ .../client/ml/GetCategoriesResponse.java | 79 ++++++++++ .../client/MLRequestConvertersTests.java | 16 ++ .../client/MachineLearningGetResultsIT.java | 141 ++++++++++++++++++ .../MlClientDocumentationIT.java | 75 +++++++++- .../client/ml/GetCategoriesRequestTests.java | 51 +++++++ .../client/ml/GetCategoriesResponseTests.java | 53 +++++++ .../job/results/CategoryDefinitionTests.java | 2 +- .../high-level/ml/get-categories.asciidoc | 83 +++++++++++ .../high-level/supported-apis.asciidoc | 2 + 12 files changed, 684 insertions(+), 2 deletions(-) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetCategoriesRequest.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetCategoriesResponse.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetCategoriesRequestTests.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetCategoriesResponseTests.java create mode 100644 docs/java-rest/high-level/ml/get-categories.asciidoc diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java index ecbe7f2d3a5..d158c1a06a2 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java @@ -32,6 +32,7 @@ import org.elasticsearch.client.ml.DeleteJobRequest; import org.elasticsearch.client.ml.FlushJobRequest; import org.elasticsearch.client.ml.ForecastJobRequest; import org.elasticsearch.client.ml.GetBucketsRequest; +import org.elasticsearch.client.ml.GetCategoriesRequest; import org.elasticsearch.client.ml.GetInfluencersRequest; import org.elasticsearch.client.ml.GetJobRequest; import org.elasticsearch.client.ml.GetJobStatsRequest; @@ -194,6 +195,20 @@ final class MLRequestConverters { return request; } + static Request getCategories(GetCategoriesRequest getCategoriesRequest) throws IOException { + String endpoint = new EndpointBuilder() + .addPathPartAsIs("_xpack") + .addPathPartAsIs("ml") + .addPathPartAsIs("anomaly_detectors") + .addPathPart(getCategoriesRequest.getJobId()) + .addPathPartAsIs("results") + .addPathPartAsIs("categories") + .build(); + Request request = new Request(HttpGet.METHOD_NAME, endpoint); + request.setEntity(createEntity(getCategoriesRequest, REQUEST_BODY_CONTENT_TYPE)); + return request; + } + static Request getOverallBuckets(GetOverallBucketsRequest getOverallBucketsRequest) throws IOException { String endpoint = new EndpointBuilder() .addPathPartAsIs("_xpack") diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java index 85c5771f345..b5f7550b913 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java @@ -32,6 +32,8 @@ import org.elasticsearch.client.ml.FlushJobRequest; import org.elasticsearch.client.ml.FlushJobResponse; import org.elasticsearch.client.ml.GetBucketsRequest; import org.elasticsearch.client.ml.GetBucketsResponse; +import org.elasticsearch.client.ml.GetCategoriesRequest; +import org.elasticsearch.client.ml.GetCategoriesResponse; import org.elasticsearch.client.ml.GetInfluencersRequest; import org.elasticsearch.client.ml.GetInfluencersResponse; import org.elasticsearch.client.ml.GetJobRequest; @@ -474,6 +476,45 @@ public final class MachineLearningClient { Collections.emptySet()); } + /** + * Gets the categories for a Machine Learning Job. + *

+ * For additional info + * see + * ML GET categories documentation + * + * @param request The request + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @throws IOException when there is a serialization issue sending the request or receiving the response + */ + public GetCategoriesResponse getCategories(GetCategoriesRequest request, RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(request, + MLRequestConverters::getCategories, + options, + GetCategoriesResponse::fromXContent, + Collections.emptySet()); + } + + /** + * Gets the categories for a Machine Learning Job, notifies listener once the requested buckets are retrieved. + *

+ * For additional info + * see + * ML GET categories documentation + * + * @param request The request + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener Listener to be notified upon request completion + */ + public void getCategoriesAsync(GetCategoriesRequest request, RequestOptions options, ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(request, + MLRequestConverters::getCategories, + options, + GetCategoriesResponse::fromXContent, + listener, + Collections.emptySet()); + } + /** * Gets overall buckets for a set of Machine Learning Jobs. *

diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetCategoriesRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetCategoriesRequest.java new file mode 100644 index 00000000000..4fc68793f00 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetCategoriesRequest.java @@ -0,0 +1,128 @@ +/* + * 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; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.client.ml.job.config.Job; +import org.elasticsearch.client.ml.job.util.PageParams; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Objects; + +/** + * A request to retrieve categories of a given job + */ +public class GetCategoriesRequest extends ActionRequest implements ToXContentObject { + + + public static final ParseField CATEGORY_ID = new ParseField("category_id"); + + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "get_categories_request", a -> new GetCategoriesRequest((String) a[0])); + + + static { + PARSER.declareString(ConstructingObjectParser.constructorArg(), Job.ID); + PARSER.declareLong(GetCategoriesRequest::setCategoryId, CATEGORY_ID); + PARSER.declareObject(GetCategoriesRequest::setPageParams, PageParams.PARSER, PageParams.PAGE); + } + + private final String jobId; + private Long categoryId; + private PageParams pageParams; + + /** + * Constructs a request to retrieve category information from a given job + * @param jobId id of the job from which to retrieve results + */ + public GetCategoriesRequest(String jobId) { + this.jobId = Objects.requireNonNull(jobId); + } + + public String getJobId() { + return jobId; + } + + public PageParams getPageParams() { + return pageParams; + } + + public Long getCategoryId() { + return categoryId; + } + + /** + * Sets the category id + * @param categoryId the category id + */ + public void setCategoryId(Long categoryId) { + this.categoryId = categoryId; + } + + /** + * Sets the paging parameters + * @param pageParams the paging parameters + */ + public void setPageParams(PageParams pageParams) { + this.pageParams = pageParams; + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(Job.ID.getPreferredName(), jobId); + if (categoryId != null) { + builder.field(CATEGORY_ID.getPreferredName(), categoryId); + } + if (pageParams != null) { + builder.field(PageParams.PAGE.getPreferredName(), pageParams); + } + builder.endObject(); + return builder; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + GetCategoriesRequest request = (GetCategoriesRequest) obj; + return Objects.equals(jobId, request.jobId) + && Objects.equals(categoryId, request.categoryId) + && Objects.equals(pageParams, request.pageParams); + } + + @Override + public int hashCode() { + return Objects.hash(jobId, categoryId, pageParams); + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetCategoriesResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetCategoriesResponse.java new file mode 100644 index 00000000000..3d3abe00bfb --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetCategoriesResponse.java @@ -0,0 +1,79 @@ +/* + * 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; + +import org.elasticsearch.client.ml.job.results.CategoryDefinition; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; + +/** + * A response containing the requested categories + */ +public class GetCategoriesResponse extends AbstractResultResponse { + + public static final ParseField CATEGORIES = new ParseField("categories"); + + @SuppressWarnings("unchecked") + public static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("get_categories_response", true, + a -> new GetCategoriesResponse((List) a[0], (long) a[1])); + + static { + PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(), CategoryDefinition.PARSER, CATEGORIES); + PARSER.declareLong(ConstructingObjectParser.constructorArg(), COUNT); + } + + public static GetCategoriesResponse fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + GetCategoriesResponse(List categories, long count) { + super(CATEGORIES, categories, count); + } + + /** + * The retrieved categories + * @return the retrieved categories + */ + public List categories() { + return results; + } + + @Override + public int hashCode() { + return Objects.hash(count, results); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + GetCategoriesResponse other = (GetCategoriesResponse) obj; + return count == other.count && Objects.equals(results, other.results); + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java index 26e6251af48..7cc5f119c39 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java @@ -28,6 +28,7 @@ import org.elasticsearch.client.ml.DeleteJobRequest; import org.elasticsearch.client.ml.FlushJobRequest; import org.elasticsearch.client.ml.ForecastJobRequest; import org.elasticsearch.client.ml.GetBucketsRequest; +import org.elasticsearch.client.ml.GetCategoriesRequest; import org.elasticsearch.client.ml.GetInfluencersRequest; import org.elasticsearch.client.ml.GetJobRequest; import org.elasticsearch.client.ml.GetJobStatsRequest; @@ -220,6 +221,21 @@ public class MLRequestConvertersTests extends ESTestCase { } } + public void testGetCategories() throws IOException { + String jobId = randomAlphaOfLength(10); + GetCategoriesRequest getCategoriesRequest = new GetCategoriesRequest(jobId); + getCategoriesRequest.setPageParams(new PageParams(100, 300)); + + + Request request = MLRequestConverters.getCategories(getCategoriesRequest); + assertEquals(HttpGet.METHOD_NAME, request.getMethod()); + assertEquals("/_xpack/ml/anomaly_detectors/" + jobId + "/results/categories", request.getEndpoint()); + try (XContentParser parser = createParser(JsonXContent.jsonXContent, request.getEntity().getContent())) { + GetCategoriesRequest parsedRequest = GetCategoriesRequest.PARSER.apply(parser, null); + assertThat(parsedRequest, equalTo(getCategoriesRequest)); + } + } + public void testGetOverallBuckets() throws IOException { String jobId = randomAlphaOfLength(10); GetOverallBucketsRequest getOverallBucketsRequest = new GetOverallBucketsRequest(jobId); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningGetResultsIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningGetResultsIT.java index 40d8596d1ba..ddaec641573 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningGetResultsIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningGetResultsIT.java @@ -23,6 +23,8 @@ import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.client.ml.GetBucketsRequest; import org.elasticsearch.client.ml.GetBucketsResponse; +import org.elasticsearch.client.ml.GetCategoriesRequest; +import org.elasticsearch.client.ml.GetCategoriesResponse; import org.elasticsearch.client.ml.GetInfluencersRequest; import org.elasticsearch.client.ml.GetInfluencersResponse; import org.elasticsearch.client.ml.GetOverallBucketsRequest; @@ -126,11 +128,150 @@ public class MachineLearningGetResultsIT extends ESRestHighLevelClientTestCase { bulkRequest.add(indexRequest); } + private void addCategoryIndexRequest(long categoryId, String categoryName, BulkRequest bulkRequest) { + IndexRequest indexRequest = new IndexRequest(RESULTS_INDEX, DOC); + indexRequest.source("{\"job_id\":\"" + JOB_ID + "\", \"category_id\": " + categoryId + ", \"terms\": \"" + + categoryName + "\", \"regex\": \".*?" + categoryName + ".*\", \"max_matching_length\": 3, \"examples\": [\"" + + categoryName + "\"]}", XContentType.JSON); + bulkRequest.add(indexRequest); + } + + private void addCategoriesIndexRequests(BulkRequest bulkRequest) { + + List categories = Arrays.asList("AAL", "JZA", "JBU"); + + for (int i = 0; i < categories.size(); i++) { + addCategoryIndexRequest(i+1, categories.get(i), bulkRequest); + } + } + @After public void deleteJob() throws IOException { new MlRestTestStateCleaner(logger, client()).clearMlMetadata(); } + public void testGetCategories() throws IOException { + + // index some category results + BulkRequest bulkRequest = new BulkRequest(); + bulkRequest.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); + + addCategoriesIndexRequests(bulkRequest); + + highLevelClient().bulk(bulkRequest, RequestOptions.DEFAULT); + + MachineLearningClient machineLearningClient = highLevelClient().machineLearning(); + + { + GetCategoriesRequest request = new GetCategoriesRequest(JOB_ID); + request.setPageParams(new PageParams(0, 10000)); + + GetCategoriesResponse response = execute(request, machineLearningClient::getCategories, + machineLearningClient::getCategoriesAsync); + + assertThat(response.count(), equalTo(3L)); + assertThat(response.categories().size(), equalTo(3)); + assertThat(response.categories().get(0).getCategoryId(), equalTo(1L)); + assertThat(response.categories().get(0).getGrokPattern(), equalTo(".*?AAL.*")); + assertThat(response.categories().get(0).getRegex(), equalTo(".*?AAL.*")); + assertThat(response.categories().get(0).getTerms(), equalTo("AAL")); + + assertThat(response.categories().get(1).getCategoryId(), equalTo(2L)); + assertThat(response.categories().get(1).getGrokPattern(), equalTo(".*?JZA.*")); + assertThat(response.categories().get(1).getRegex(), equalTo(".*?JZA.*")); + assertThat(response.categories().get(1).getTerms(), equalTo("JZA")); + + assertThat(response.categories().get(2).getCategoryId(), equalTo(3L)); + assertThat(response.categories().get(2).getGrokPattern(), equalTo(".*?JBU.*")); + assertThat(response.categories().get(2).getRegex(), equalTo(".*?JBU.*")); + assertThat(response.categories().get(2).getTerms(), equalTo("JBU")); + } + { + GetCategoriesRequest request = new GetCategoriesRequest(JOB_ID); + request.setPageParams(new PageParams(0, 1)); + + GetCategoriesResponse response = execute(request, machineLearningClient::getCategories, + machineLearningClient::getCategoriesAsync); + + assertThat(response.count(), equalTo(3L)); + assertThat(response.categories().size(), equalTo(1)); + assertThat(response.categories().get(0).getCategoryId(), equalTo(1L)); + assertThat(response.categories().get(0).getGrokPattern(), equalTo(".*?AAL.*")); + assertThat(response.categories().get(0).getRegex(), equalTo(".*?AAL.*")); + assertThat(response.categories().get(0).getTerms(), equalTo("AAL")); + } + { + GetCategoriesRequest request = new GetCategoriesRequest(JOB_ID); + request.setPageParams(new PageParams(1, 2)); + + GetCategoriesResponse response = execute(request, machineLearningClient::getCategories, + machineLearningClient::getCategoriesAsync); + + assertThat(response.count(), equalTo(3L)); + assertThat(response.categories().size(), equalTo(2)); + assertThat(response.categories().get(0).getCategoryId(), equalTo(2L)); + assertThat(response.categories().get(0).getGrokPattern(), equalTo(".*?JZA.*")); + assertThat(response.categories().get(0).getRegex(), equalTo(".*?JZA.*")); + assertThat(response.categories().get(0).getTerms(), equalTo("JZA")); + + assertThat(response.categories().get(1).getCategoryId(), equalTo(3L)); + assertThat(response.categories().get(1).getGrokPattern(), equalTo(".*?JBU.*")); + assertThat(response.categories().get(1).getRegex(), equalTo(".*?JBU.*")); + assertThat(response.categories().get(1).getTerms(), equalTo("JBU")); + } + { + GetCategoriesRequest request = new GetCategoriesRequest(JOB_ID); + request.setCategoryId(0L); // request a non-existent category + + GetCategoriesResponse response = execute(request, machineLearningClient::getCategories, + machineLearningClient::getCategoriesAsync); + + assertThat(response.count(), equalTo(0L)); + assertThat(response.categories().size(), equalTo(0)); + } + { + GetCategoriesRequest request = new GetCategoriesRequest(JOB_ID); + request.setCategoryId(1L); + + GetCategoriesResponse response = execute(request, machineLearningClient::getCategories, + machineLearningClient::getCategoriesAsync); + + assertThat(response.count(), equalTo(1L)); + assertThat(response.categories().size(), equalTo(1)); + assertThat(response.categories().get(0).getCategoryId(), equalTo(1L)); + assertThat(response.categories().get(0).getGrokPattern(), equalTo(".*?AAL.*")); + assertThat(response.categories().get(0).getRegex(), equalTo(".*?AAL.*")); + assertThat(response.categories().get(0).getTerms(), equalTo("AAL")); + } + { + GetCategoriesRequest request = new GetCategoriesRequest(JOB_ID); + request.setCategoryId(2L); + + GetCategoriesResponse response = execute(request, machineLearningClient::getCategories, + machineLearningClient::getCategoriesAsync); + + assertThat(response.count(), equalTo(1L)); + assertThat(response.categories().get(0).getCategoryId(), equalTo(2L)); + assertThat(response.categories().get(0).getGrokPattern(), equalTo(".*?JZA.*")); + assertThat(response.categories().get(0).getRegex(), equalTo(".*?JZA.*")); + assertThat(response.categories().get(0).getTerms(), equalTo("JZA")); + + } + { + GetCategoriesRequest request = new GetCategoriesRequest(JOB_ID); + request.setCategoryId(3L); + + GetCategoriesResponse response = execute(request, machineLearningClient::getCategories, + machineLearningClient::getCategoriesAsync); + + assertThat(response.count(), equalTo(1L)); + assertThat(response.categories().get(0).getCategoryId(), equalTo(3L)); + assertThat(response.categories().get(0).getGrokPattern(), equalTo(".*?JBU.*")); + assertThat(response.categories().get(0).getRegex(), equalTo(".*?JBU.*")); + assertThat(response.categories().get(0).getTerms(), equalTo("JBU")); + } + } + public void testGetBuckets() throws IOException { MachineLearningClient machineLearningClient = highLevelClient().machineLearning(); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java index 9abef54d0d2..845729eccbd 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java @@ -39,6 +39,8 @@ import org.elasticsearch.client.ml.ForecastJobRequest; import org.elasticsearch.client.ml.ForecastJobResponse; import org.elasticsearch.client.ml.GetBucketsRequest; import org.elasticsearch.client.ml.GetBucketsResponse; +import org.elasticsearch.client.ml.GetCategoriesRequest; +import org.elasticsearch.client.ml.GetCategoriesResponse; import org.elasticsearch.client.ml.GetInfluencersRequest; import org.elasticsearch.client.ml.GetInfluencersResponse; import org.elasticsearch.client.ml.GetJobRequest; @@ -69,6 +71,7 @@ import org.elasticsearch.client.ml.job.config.Operator; import org.elasticsearch.client.ml.job.config.RuleCondition; import org.elasticsearch.client.ml.job.results.AnomalyRecord; import org.elasticsearch.client.ml.job.results.Bucket; +import org.elasticsearch.client.ml.job.results.CategoryDefinition; import org.elasticsearch.client.ml.job.results.Influencer; import org.elasticsearch.client.ml.job.results.OverallBucket; import org.elasticsearch.client.ml.job.stats.JobStats; @@ -473,7 +476,7 @@ public class MlClientDocumentationIT extends ESRestHighLevelClientTestCase { assertTrue(latch.await(30L, TimeUnit.SECONDS)); } } - + public void testGetBuckets() throws IOException, InterruptedException { RestHighLevelClient client = highLevelClient(); @@ -1111,4 +1114,74 @@ public class MlClientDocumentationIT extends ESRestHighLevelClientTestCase { assertTrue(latch.await(30L, TimeUnit.SECONDS)); } } + + public void testGetCategories() throws IOException, InterruptedException { + RestHighLevelClient client = highLevelClient(); + + String jobId = "test-get-categories"; + Job job = MachineLearningIT.buildJob(jobId); + client.machineLearning().putJob(new PutJobRequest(job), RequestOptions.DEFAULT); + + // Let us index a category + IndexRequest indexRequest = new IndexRequest(".ml-anomalies-shared", "doc"); + indexRequest.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); + indexRequest.source("{\"job_id\": \"test-get-categories\", \"category_id\": 1, \"terms\": \"AAL\"," + + " \"regex\": \".*?AAL.*\", \"max_matching_length\": 3, \"examples\": [\"AAL\"]}", XContentType.JSON); + client.index(indexRequest, RequestOptions.DEFAULT); + + { + // tag::x-pack-ml-get-categories-request + GetCategoriesRequest request = new GetCategoriesRequest(jobId); // <1> + // end::x-pack-ml-get-categories-request + + // tag::x-pack-ml-get-categories-category-id + request.setCategoryId(1L); // <1> + // end::x-pack-ml-get-categories-category-id + + // tag::x-pack-ml-get-categories-page + request.setPageParams(new PageParams(100, 200)); // <1> + // end::x-pack-ml-get-categories-page + + // Set page params back to null so the response contains the category we indexed + request.setPageParams(null); + + // tag::x-pack-ml-get-categories-execute + GetCategoriesResponse response = client.machineLearning().getCategories(request, RequestOptions.DEFAULT); + // end::x-pack-ml-get-categories-execute + + // tag::x-pack-ml-get-categories-response + long count = response.count(); // <1> + List categories = response.categories(); // <2> + // end::x-pack-ml-get-categories-response + assertEquals(1, categories.size()); + } + { + GetCategoriesRequest request = new GetCategoriesRequest(jobId); + + // tag::x-pack-ml-get-categories-listener + ActionListener listener = + new ActionListener() { + @Override + public void onResponse(GetCategoriesResponse getcategoriesResponse) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::x-pack-ml-get-categories-listener + + // Replace the empty listener by a blocking listener in test + final CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + + // tag::x-pack-ml-get-categories-execute-async + client.machineLearning().getCategoriesAsync(request, RequestOptions.DEFAULT, listener); // <1> + // end::x-pack-ml-get-categories-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetCategoriesRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetCategoriesRequestTests.java new file mode 100644 index 00000000000..7d9fe2b238f --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetCategoriesRequestTests.java @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.client.ml; + +import org.elasticsearch.client.ml.job.util.PageParams; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; + +public class GetCategoriesRequestTests extends AbstractXContentTestCase { + + @Override + protected GetCategoriesRequest createTestInstance() { + GetCategoriesRequest request = new GetCategoriesRequest(randomAlphaOfLengthBetween(1, 20)); + if (randomBoolean()) { + request.setCategoryId(randomNonNegativeLong()); + } else { + int from = randomInt(10000); + int size = randomInt(10000); + request.setPageParams(new PageParams(from, size)); + } + return request; + } + + @Override + protected GetCategoriesRequest doParseInstance(XContentParser parser) throws IOException { + return GetCategoriesRequest.PARSER.apply(parser, null); + } + + @Override + protected boolean supportsUnknownFields() { + return false; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetCategoriesResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetCategoriesResponseTests.java new file mode 100644 index 00000000000..e8718ba20e9 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetCategoriesResponseTests.java @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.client.ml; + +import org.elasticsearch.client.ml.job.results.CategoryDefinition; +import org.elasticsearch.client.ml.job.results.CategoryDefinitionTests; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class GetCategoriesResponseTests extends AbstractXContentTestCase { + + @Override + protected GetCategoriesResponse createTestInstance() { + String jobId = randomAlphaOfLength(20); + int listSize = randomInt(10); + List categories = new ArrayList<>(listSize); + for (int j = 0; j < listSize; j++) { + CategoryDefinition category = CategoryDefinitionTests.createTestInstance(jobId); + categories.add(category); + } + return new GetCategoriesResponse(categories, listSize); + } + + @Override + protected GetCategoriesResponse doParseInstance(XContentParser parser) throws IOException { + return GetCategoriesResponse.fromXContent(parser); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/CategoryDefinitionTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/CategoryDefinitionTests.java index 27e15a1600d..63f26158386 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/CategoryDefinitionTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/CategoryDefinitionTests.java @@ -25,7 +25,7 @@ import java.util.Arrays; public class CategoryDefinitionTests extends AbstractXContentTestCase { - public CategoryDefinition createTestInstance(String jobId) { + public static CategoryDefinition createTestInstance(String jobId) { CategoryDefinition categoryDefinition = new CategoryDefinition(jobId); categoryDefinition.setCategoryId(randomLong()); categoryDefinition.setTerms(randomAlphaOfLength(10)); diff --git a/docs/java-rest/high-level/ml/get-categories.asciidoc b/docs/java-rest/high-level/ml/get-categories.asciidoc new file mode 100644 index 00000000000..0e86a2b7f33 --- /dev/null +++ b/docs/java-rest/high-level/ml/get-categories.asciidoc @@ -0,0 +1,83 @@ +[[java-rest-high-x-pack-ml-get-categories]] +=== Get Categories API + +The Get Categories API retrieves one or more category results. +It accepts a `GetCategoriesRequest` object and responds +with a `GetCategoriesResponse` object. + +[[java-rest-high-x-pack-ml-get-categories-request]] +==== Get Categories Request + +A `GetCategoriesRequest` object gets created with an existing non-null `jobId`. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-categories-request] +-------------------------------------------------- +<1> Constructing a new request referencing an existing `jobId` + +==== Optional Arguments +The following arguments are optional: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-categories-category-id] +-------------------------------------------------- +<1> The id of the category to get. Otherwise it will return all categories. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-categories-page] +-------------------------------------------------- +<1> The page parameters `from` and `size`. `from` specifies the number of categories to skip. +`size` specifies the maximum number of categories to get. Defaults to `0` and `100` respectively. + +[[java-rest-high-x-pack-ml-get-categories-execution]] +==== Execution + +The request can be executed through the `MachineLearningClient` contained +in the `RestHighLevelClient` object, accessed via the `machineLearningClient()` method. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-categories-execute] +-------------------------------------------------- + + +[[java-rest-high-x-pack-ml-get-categories-execution-async]] +==== Asynchronous Execution + +The request can also be executed asynchronously: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-categories-execute-async] +-------------------------------------------------- +<1> The `GetCategoriesRequest` to execute and the `ActionListener` to use when +the execution completes + +The asynchronous method does not block and returns immediately. Once it is +completed the `ActionListener` is called back with the `onResponse` method +if the execution is successful or the `onFailure` method if the execution +failed. + +A typical listener for `GetCategoriesResponse` looks like: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-categories-listener] +-------------------------------------------------- +<1> `onResponse` is called back when the action is completed successfully +<2> `onFailure` is called back when some unexpected error occurs + +[[java-rest-high-snapshot-ml-get-categories-response]] +==== Get Categories Response + +The returned `GetCategoriesResponse` contains the requested categories: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-categories-response] +-------------------------------------------------- +<1> The count of categories that were matched +<2> The categories retrieved \ No newline at end of file diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index 8d92653ce57..87639a2ea3f 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -226,6 +226,7 @@ The Java High Level REST Client supports the following Machine Learning APIs: * <> * <> * <> +* <> include::ml/put-job.asciidoc[] include::ml/get-job.asciidoc[] @@ -241,6 +242,7 @@ include::ml/get-overall-buckets.asciidoc[] include::ml/get-records.asciidoc[] include::ml/post-data.asciidoc[] include::ml/get-influencers.asciidoc[] +include::ml/get-categories.asciidoc[] == Migration APIs From 517cfc3cc0c2278a1287cf961c7db513e81dcbb6 Mon Sep 17 00:00:00 2001 From: Simon Willnauer Date: Tue, 11 Sep 2018 14:05:14 +0200 Subject: [PATCH 06/78] Add read-only Engine (#33563) This change adds an engine implementation that opens a reader on an existing index but doesn't permit any refreshes or modifications to the index. Relates to #32867 Relates to #32844 --- .../elasticsearch/index/engine/Engine.java | 6 +- .../index/engine/InternalEngine.java | 5 - .../index/engine/ReadOnlyEngine.java | 372 ++++++++++++++++++ .../index/engine/InternalEngineTests.java | 2 +- .../index/engine/ReadOnlyEngineTests.java | 156 ++++++++ 5 files changed, 533 insertions(+), 8 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/index/engine/ReadOnlyEngine.java create mode 100644 server/src/test/java/org/elasticsearch/index/engine/ReadOnlyEngineTests.java diff --git a/server/src/main/java/org/elasticsearch/index/engine/Engine.java b/server/src/main/java/org/elasticsearch/index/engine/Engine.java index fe27aea805e..ea8161c1589 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/Engine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/Engine.java @@ -661,7 +661,7 @@ public abstract class Engine implements Closeable { } /** get commits stats for the last commit */ - public CommitStats commitStats() { + public final CommitStats commitStats() { return new CommitStats(getLastCommittedSegmentInfos()); } @@ -951,7 +951,9 @@ public abstract class Engine implements Closeable { * * @return the commit Id for the resulting commit */ - public abstract CommitId flush() throws EngineException; + public final CommitId flush() throws EngineException { + return flush(false, false); + } /** diff --git a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java index d9b03777f1b..b2ab0d71c32 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java @@ -1576,11 +1576,6 @@ public class InternalEngine extends Engine { || localCheckpointTracker.getCheckpoint() == localCheckpointTracker.getMaxSeqNo(); } - @Override - public CommitId flush() throws EngineException { - return flush(false, false); - } - @Override public CommitId flush(boolean force, boolean waitIfOngoing) throws EngineException { ensureOpen(); diff --git a/server/src/main/java/org/elasticsearch/index/engine/ReadOnlyEngine.java b/server/src/main/java/org/elasticsearch/index/engine/ReadOnlyEngine.java new file mode 100644 index 00000000000..a55987d0a00 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/engine/ReadOnlyEngine.java @@ -0,0 +1,372 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.index.engine; + +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexCommit; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.SegmentInfos; +import org.apache.lucene.index.SoftDeletesDirectoryReaderWrapper; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.ReferenceManager; +import org.apache.lucene.search.SearcherManager; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.Lock; +import org.elasticsearch.common.lucene.Lucene; +import org.elasticsearch.common.lucene.index.ElasticsearchDirectoryReader; +import org.elasticsearch.core.internal.io.IOUtils; +import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.index.seqno.SeqNoStats; +import org.elasticsearch.index.seqno.SequenceNumbers; +import org.elasticsearch.index.store.Store; +import org.elasticsearch.index.translog.Translog; +import org.elasticsearch.index.translog.TranslogStats; + +import java.io.Closeable; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Stream; + +/** + * A basic read-only engine that allows switching a shard to be true read-only temporarily or permanently. + * Note: this engine can be opened side-by-side with a read-write engine but will not reflect any changes made to the read-write + * engine. + * + * @see #ReadOnlyEngine(EngineConfig, SeqNoStats, TranslogStats, boolean, Function) + */ +public final class ReadOnlyEngine extends Engine { + + private final SegmentInfos lastCommittedSegmentInfos; + private final SeqNoStats seqNoStats; + private final TranslogStats translogStats; + private final SearcherManager searcherManager; + private final IndexCommit indexCommit; + private final Lock indexWriterLock; + + /** + * Creates a new ReadOnlyEngine. This ctor can also be used to open a read-only engine on top of an already opened + * read-write engine. It allows to optionally obtain the writer locks for the shard which would time-out if another + * engine is still open. + * + * @param config the engine configuration + * @param seqNoStats sequence number statistics for this engine or null if not provided + * @param translogStats translog stats for this engine or null if not provided + * @param obtainLock if true this engine will try to obtain the {@link IndexWriter#WRITE_LOCK_NAME} lock. Otherwise + * the lock won't be obtained + * @param readerWrapperFunction allows to wrap the index-reader for this engine. + */ + public ReadOnlyEngine(EngineConfig config, SeqNoStats seqNoStats, TranslogStats translogStats, boolean obtainLock, + Function readerWrapperFunction) { + super(config); + try { + Store store = config.getStore(); + store.incRef(); + DirectoryReader reader = null; + Directory directory = store.directory(); + Lock indexWriterLock = null; + boolean success = false; + try { + // we obtain the IW lock even though we never modify the index. + // yet this makes sure nobody else does. including some testing tools that try to be messy + indexWriterLock = obtainLock ? directory.obtainLock(IndexWriter.WRITE_LOCK_NAME) : null; + this.lastCommittedSegmentInfos = Lucene.readSegmentInfos(directory); + this.translogStats = translogStats == null ? new TranslogStats(0, 0, 0, 0, 0) : translogStats; + this.seqNoStats = seqNoStats == null ? buildSeqNoStats(lastCommittedSegmentInfos) : seqNoStats; + reader = ElasticsearchDirectoryReader.wrap(DirectoryReader.open(directory), config.getShardId()); + if (config.getIndexSettings().isSoftDeleteEnabled()) { + reader = new SoftDeletesDirectoryReaderWrapper(reader, Lucene.SOFT_DELETES_FIELD); + } + reader = readerWrapperFunction.apply(reader); + this.indexCommit = reader.getIndexCommit(); + this.searcherManager = new SearcherManager(reader, + new RamAccountingSearcherFactory(engineConfig.getCircuitBreakerService())); + this.indexWriterLock = indexWriterLock; + success = true; + } finally { + if (success == false) { + IOUtils.close(reader, indexWriterLock, store::decRef); + } + } + } catch (IOException e) { + throw new UncheckedIOException(e); // this is stupid + } + } + + @Override + protected void closeNoLock(String reason, CountDownLatch closedLatch) { + if (isClosed.compareAndSet(false, true)) { + try { + IOUtils.close(searcherManager, indexWriterLock, store::decRef); + } catch (Exception ex) { + logger.warn("failed to close searcher", ex); + } finally { + closedLatch.countDown(); + } + } + } + + public static SeqNoStats buildSeqNoStats(SegmentInfos infos) { + final SequenceNumbers.CommitInfo seqNoStats = + SequenceNumbers.loadSeqNoInfoFromLuceneCommit(infos.userData.entrySet()); + long maxSeqNo = seqNoStats.maxSeqNo; + long localCheckpoint = seqNoStats.localCheckpoint; + return new SeqNoStats(maxSeqNo, localCheckpoint, localCheckpoint); + } + + @Override + public GetResult get(Get get, BiFunction searcherFactory) throws EngineException { + return getFromSearcher(get, searcherFactory, SearcherScope.EXTERNAL); + } + + @Override + protected ReferenceManager getReferenceManager(SearcherScope scope) { + return searcherManager; + } + + @Override + protected SegmentInfos getLastCommittedSegmentInfos() { + return lastCommittedSegmentInfos; + } + + @Override + public String getHistoryUUID() { + return lastCommittedSegmentInfos.userData.get(Engine.HISTORY_UUID_KEY); + } + + @Override + public long getWritingBytes() { + return 0; + } + + @Override + public long getIndexThrottleTimeInMillis() { + return 0; + } + + @Override + public boolean isThrottled() { + return false; + } + + @Override + public IndexResult index(Index index) { + assert false : "this should not be called"; + throw new UnsupportedOperationException("indexing is not supported on a read-only engine"); + } + + @Override + public DeleteResult delete(Delete delete) { + assert false : "this should not be called"; + throw new UnsupportedOperationException("deletes are not supported on a read-only engine"); + } + + @Override + public NoOpResult noOp(NoOp noOp) { + assert false : "this should not be called"; + throw new UnsupportedOperationException("no-ops are not supported on a read-only engine"); + } + + @Override + public boolean isTranslogSyncNeeded() { + return false; + } + + @Override + public boolean ensureTranslogSynced(Stream locations) { + return false; + } + + @Override + public void syncTranslog() { + } + + @Override + public Closeable acquireRetentionLockForPeerRecovery() { + return () -> {}; + } + + @Override + public Translog.Snapshot newChangesSnapshot(String source, MapperService mapperService, long fromSeqNo, long toSeqNo, + boolean requiredFullRange) throws IOException { + return readHistoryOperations(source, mapperService, fromSeqNo); + } + + @Override + public Translog.Snapshot readHistoryOperations(String source, MapperService mapperService, long startingSeqNo) throws IOException { + return new Translog.Snapshot() { + @Override + public void close() { } + @Override + public int totalOperations() { + return 0; + } + @Override + public Translog.Operation next() { + return null; + } + }; + } + + @Override + public int estimateNumberOfHistoryOperations(String source, MapperService mapperService, long startingSeqNo) throws IOException { + return 0; + } + + @Override + public boolean hasCompleteOperationHistory(String source, MapperService mapperService, long startingSeqNo) throws IOException { + return false; + } + + @Override + public TranslogStats getTranslogStats() { + return translogStats; + } + + @Override + public Translog.Location getTranslogLastWriteLocation() { + return new Translog.Location(0,0,0); + } + + @Override + public long getLocalCheckpoint() { + return seqNoStats.getLocalCheckpoint(); + } + + @Override + public void waitForOpsToComplete(long seqNo) { + } + + @Override + public void resetLocalCheckpoint(long newCheckpoint) { + } + + @Override + public SeqNoStats getSeqNoStats(long globalCheckpoint) { + return new SeqNoStats(seqNoStats.getMaxSeqNo(), seqNoStats.getLocalCheckpoint(), globalCheckpoint); + } + + @Override + public long getLastSyncedGlobalCheckpoint() { + return seqNoStats.getGlobalCheckpoint(); + } + + @Override + public long getIndexBufferRAMBytesUsed() { + return 0; + } + + @Override + public List segments(boolean verbose) { + return Arrays.asList(getSegmentInfo(lastCommittedSegmentInfos, verbose)); + } + + @Override + public void refresh(String source) { + // we could allow refreshes if we want down the road the searcher manager will then reflect changes to a rw-engine + // opened side-by-side + } + + @Override + public void writeIndexingBuffer() throws EngineException { + } + + @Override + public boolean shouldPeriodicallyFlush() { + return false; + } + + @Override + public SyncedFlushResult syncFlush(String syncId, CommitId expectedCommitId) { + // we can't do synced flushes this would require an indexWriter which we don't have + throw new UnsupportedOperationException("syncedFlush is not supported on a read-only engine"); + } + + @Override + public CommitId flush(boolean force, boolean waitIfOngoing) throws EngineException { + return new CommitId(lastCommittedSegmentInfos.getId()); + } + + @Override + public void forceMerge(boolean flush, int maxNumSegments, boolean onlyExpungeDeletes, + boolean upgrade, boolean upgradeOnlyAncientSegments) { + } + + @Override + public IndexCommitRef acquireLastIndexCommit(boolean flushFirst) { + store.incRef(); + return new IndexCommitRef(indexCommit, store::decRef); + } + + @Override + public IndexCommitRef acquireSafeIndexCommit() { + return acquireLastIndexCommit(false); + } + + @Override + public void activateThrottling() { + } + + @Override + public void deactivateThrottling() { + } + + @Override + public void trimUnreferencedTranslogFiles() { + } + + @Override + public boolean shouldRollTranslogGeneration() { + return false; + } + + @Override + public void rollTranslogGeneration() { + } + + @Override + public void restoreLocalCheckpointFromTranslog() { + } + + @Override + public int fillSeqNoGaps(long primaryTerm) { + return 0; + } + + @Override + public Engine recoverFromTranslog(TranslogRecoveryRunner translogRecoveryRunner, long recoverUpToSeqNo) { + return this; + } + + @Override + public void skipTranslogRecovery() { + } + + @Override + public void trimOperationsFromTranslog(long belowTerm, long aboveSeqNo) { + } + + @Override + public void maybePruneDeletes() { + } +} diff --git a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java index a26fd72468b..39132d805b2 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java @@ -5033,7 +5033,7 @@ public class InternalEngineTests extends EngineTestCase { expectThrows(AlreadyClosedException.class, () -> engine.acquireSearcher("test")); } - private static void trimUnsafeCommits(EngineConfig config) throws IOException { + static void trimUnsafeCommits(EngineConfig config) throws IOException { final Store store = config.getStore(); final TranslogConfig translogConfig = config.getTranslogConfig(); final String translogUUID = store.readLastCommittedSegmentsInfo().getUserData().get(Translog.TRANSLOG_UUID_KEY); diff --git a/server/src/test/java/org/elasticsearch/index/engine/ReadOnlyEngineTests.java b/server/src/test/java/org/elasticsearch/index/engine/ReadOnlyEngineTests.java new file mode 100644 index 00000000000..4a5b89351bd --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/engine/ReadOnlyEngineTests.java @@ -0,0 +1,156 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.index.engine; + +import org.apache.lucene.util.LuceneTestCase; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.core.internal.io.IOUtils; +import org.elasticsearch.index.mapper.ParsedDocument; +import org.elasticsearch.index.seqno.SeqNoStats; +import org.elasticsearch.index.seqno.SequenceNumbers; +import org.elasticsearch.index.store.Store; + +import java.io.IOException; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; + +import static org.hamcrest.Matchers.equalTo; + +public class ReadOnlyEngineTests extends EngineTestCase { + + public void testReadOnlyEngine() throws Exception { + IOUtils.close(engine, store); + Engine readOnlyEngine = null; + final AtomicLong globalCheckpoint = new AtomicLong(SequenceNumbers.NO_OPS_PERFORMED); + try (Store store = createStore()) { + EngineConfig config = config(defaultSettings, store, createTempDir(), newMergePolicy(), null, null, globalCheckpoint::get); + int numDocs = scaledRandomIntBetween(10, 1000); + final SeqNoStats lastSeqNoStats; + final Set lastDocIds; + try (InternalEngine engine = createEngine(config)) { + Engine.Get get = null; + for (int i = 0; i < numDocs; i++) { + if (rarely()) { + continue; // gap in sequence number + } + ParsedDocument doc = testParsedDocument(Integer.toString(i), null, testDocument(), new BytesArray("{}"), null); + engine.index(new Engine.Index(newUid(doc), doc, i, primaryTerm.get(), 1, null, Engine.Operation.Origin.REPLICA, + System.nanoTime(), -1, false)); + if (get == null || rarely()) { + get = newGet(randomBoolean(), doc); + } + if (rarely()) { + engine.flush(); + } + globalCheckpoint.set(randomLongBetween(globalCheckpoint.get(), engine.getLocalCheckpoint())); + } + engine.syncTranslog(); + engine.flush(); + readOnlyEngine = new ReadOnlyEngine(engine.engineConfig, engine.getSeqNoStats(globalCheckpoint.get()), + engine.getTranslogStats(), false, Function.identity()); + lastSeqNoStats = engine.getSeqNoStats(globalCheckpoint.get()); + lastDocIds = getDocIds(engine, true); + assertThat(readOnlyEngine.getLocalCheckpoint(), equalTo(lastSeqNoStats.getLocalCheckpoint())); + assertThat(readOnlyEngine.getSeqNoStats(globalCheckpoint.get()).getMaxSeqNo(), equalTo(lastSeqNoStats.getMaxSeqNo())); + assertThat(getDocIds(readOnlyEngine, false), equalTo(lastDocIds)); + for (int i = 0; i < numDocs; i++) { + if (randomBoolean()) { + String delId = Integer.toString(i); + engine.delete(new Engine.Delete("test", delId, newUid(delId), primaryTerm.get())); + } + if (rarely()) { + engine.flush(); + } + } + Engine.Searcher external = readOnlyEngine.acquireSearcher("test", Engine.SearcherScope.EXTERNAL); + Engine.Searcher internal = readOnlyEngine.acquireSearcher("test", Engine.SearcherScope.INTERNAL); + assertSame(external.reader(), internal.reader()); + IOUtils.close(external, internal); + // the locked down engine should still point to the previous commit + assertThat(readOnlyEngine.getLocalCheckpoint(), equalTo(lastSeqNoStats.getLocalCheckpoint())); + assertThat(readOnlyEngine.getSeqNoStats(globalCheckpoint.get()).getMaxSeqNo(), equalTo(lastSeqNoStats.getMaxSeqNo())); + assertThat(getDocIds(readOnlyEngine, false), equalTo(lastDocIds)); + try (Engine.GetResult getResult = readOnlyEngine.get(get, readOnlyEngine::acquireSearcher)) { + assertTrue(getResult.exists()); + } + + } + // Close and reopen the main engine + InternalEngineTests.trimUnsafeCommits(config); + try (InternalEngine recoveringEngine = new InternalEngine(config)) { + recoveringEngine.recoverFromTranslog(translogHandler, Long.MAX_VALUE); + // the locked down engine should still point to the previous commit + assertThat(readOnlyEngine.getLocalCheckpoint(), equalTo(lastSeqNoStats.getLocalCheckpoint())); + assertThat(readOnlyEngine.getSeqNoStats(globalCheckpoint.get()).getMaxSeqNo(), equalTo(lastSeqNoStats.getMaxSeqNo())); + assertThat(getDocIds(readOnlyEngine, false), equalTo(lastDocIds)); + } + } finally { + IOUtils.close(readOnlyEngine); + } + } + + public void testFlushes() throws IOException { + IOUtils.close(engine, store); + Engine readOnlyEngine = null; + final AtomicLong globalCheckpoint = new AtomicLong(SequenceNumbers.NO_OPS_PERFORMED); + try (Store store = createStore()) { + EngineConfig config = config(defaultSettings, store, createTempDir(), newMergePolicy(), null, null, globalCheckpoint::get); + int numDocs = scaledRandomIntBetween(10, 1000); + try (InternalEngine engine = createEngine(config)) { + for (int i = 0; i < numDocs; i++) { + if (rarely()) { + continue; // gap in sequence number + } + ParsedDocument doc = testParsedDocument(Integer.toString(i), null, testDocument(), new BytesArray("{}"), null); + engine.index(new Engine.Index(newUid(doc), doc, i, primaryTerm.get(), 1, null, Engine.Operation.Origin.REPLICA, + System.nanoTime(), -1, false)); + if (rarely()) { + engine.flush(); + } + globalCheckpoint.set(randomLongBetween(globalCheckpoint.get(), engine.getLocalCheckpoint())); + } + engine.syncTranslog(); + engine.flushAndClose(); + readOnlyEngine = new ReadOnlyEngine(engine.engineConfig, null , null, true, Function.identity()); + Engine.CommitId flush = readOnlyEngine.flush(randomBoolean(), randomBoolean()); + assertEquals(flush, readOnlyEngine.flush(randomBoolean(), randomBoolean())); + } finally { + IOUtils.close(readOnlyEngine); + } + } + } + + public void testReadOnly() throws IOException { + IOUtils.close(engine, store); + final AtomicLong globalCheckpoint = new AtomicLong(SequenceNumbers.NO_OPS_PERFORMED); + try (Store store = createStore()) { + EngineConfig config = config(defaultSettings, store, createTempDir(), newMergePolicy(), null, null, globalCheckpoint::get); + store.createEmpty(); + try (ReadOnlyEngine readOnlyEngine = new ReadOnlyEngine(config, null , null, true, Function.identity())) { + Class expectedException = LuceneTestCase.TEST_ASSERTS_ENABLED ? AssertionError.class : + UnsupportedOperationException.class; + expectThrows(expectedException, () -> readOnlyEngine.index(null)); + expectThrows(expectedException, () -> readOnlyEngine.delete(null)); + expectThrows(expectedException, () -> readOnlyEngine.noOp(null)); + expectThrows(UnsupportedOperationException.class, () -> readOnlyEngine.syncFlush(null, null)); + } + } + } +} From ad4b5e427004351ae7e88dadfe4210db437ab764 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Tue, 11 Sep 2018 08:35:42 -0400 Subject: [PATCH 07/78] Fix upgrading of list settings (#33589) Upgrading list settings is broken because of the conversion that we do to strings, and then when we try to put back the upgraded value we do not know that it is a representation of a list. This commit addresses this by adding special handling for list settings. --- .../settings/AbstractScopedSettings.java | 26 ++++++----- .../common/settings/SettingUpgrader.java | 6 +++ .../common/settings/ScopedSettingsTests.java | 44 +++++++++++++++++++ 3 files changed, 65 insertions(+), 11 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java b/server/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java index e25d954aa4f..b010d7982fd 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java @@ -27,7 +27,6 @@ import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.component.AbstractComponent; import org.elasticsearch.common.regex.Regex; -import java.util.AbstractMap; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -54,7 +53,7 @@ public abstract class AbstractScopedSettings extends AbstractComponent { private final List> settingUpdaters = new CopyOnWriteArrayList<>(); private final Map> complexMatchers; private final Map> keySettings; - private final Map, Function, Map.Entry>> settingUpgraders; + private final Map, SettingUpgrader> settingUpgraders; private final Setting.Property scope; private static final Pattern KEY_PATTERN = Pattern.compile("^(?:[-\\w]+[.])*[-\\w]+$"); private static final Pattern GROUP_KEY_PATTERN = Pattern.compile("^(?:[-\\w]+[.])+$"); @@ -70,12 +69,8 @@ public abstract class AbstractScopedSettings extends AbstractComponent { this.settingUpgraders = Collections.unmodifiableMap( - settingUpgraders - .stream() - .collect( - Collectors.toMap( - SettingUpgrader::getSetting, - u -> e -> new AbstractMap.SimpleEntry<>(u.getKey(e.getKey()), u.getValue(e.getValue()))))); + settingUpgraders.stream().collect(Collectors.toMap(SettingUpgrader::getSetting, Function.identity()))); + this.scope = scope; Map> complexMatchers = new HashMap<>(); @@ -786,15 +781,24 @@ public abstract class AbstractScopedSettings extends AbstractComponent { boolean changed = false; // track if any settings were upgraded for (final String key : settings.keySet()) { final Setting setting = getRaw(key); - final Function, Map.Entry> upgrader = settingUpgraders.get(setting); + final SettingUpgrader upgrader = settingUpgraders.get(setting); if (upgrader == null) { // the setting does not have an upgrader, copy the setting builder.copy(key, settings); } else { // the setting has an upgrader, so mark that we have changed a setting and apply the upgrade logic changed = true; - final Map.Entry upgrade = upgrader.apply(new Entry(key, settings)); - builder.put(upgrade.getKey(), upgrade.getValue()); + if (setting.isListSetting()) { + final List value = settings.getAsList(key); + final String upgradedKey = upgrader.getKey(key); + final List upgradedValue = upgrader.getListValue(value); + builder.putList(upgradedKey, upgradedValue); + } else { + final String value = settings.get(key); + final String upgradedKey = upgrader.getKey(key); + final String upgradedValue = upgrader.getValue(value); + builder.put(upgradedKey, upgradedValue); + } } } // we only return a new instance if there was an upgrade diff --git a/server/src/main/java/org/elasticsearch/common/settings/SettingUpgrader.java b/server/src/main/java/org/elasticsearch/common/settings/SettingUpgrader.java index 91f2bead300..bc41b554905 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/SettingUpgrader.java +++ b/server/src/main/java/org/elasticsearch/common/settings/SettingUpgrader.java @@ -19,6 +19,8 @@ package org.elasticsearch.common.settings; +import java.util.List; + /** * Represents the logic to upgrade a setting. * @@ -51,4 +53,8 @@ public interface SettingUpgrader { return value; } + default List getListValue(final List value) { + return value; + } + } diff --git a/server/src/test/java/org/elasticsearch/common/settings/ScopedSettingsTests.java b/server/src/test/java/org/elasticsearch/common/settings/ScopedSettingsTests.java index 0ee1d2e9c4a..6766316fafd 100644 --- a/server/src/test/java/org/elasticsearch/common/settings/ScopedSettingsTests.java +++ b/server/src/test/java/org/elasticsearch/common/settings/ScopedSettingsTests.java @@ -47,6 +47,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; +import java.util.stream.Collectors; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.equalTo; @@ -1171,4 +1172,47 @@ public class ScopedSettingsTests extends ESTestCase { } } + public void testUpgradeListSetting() { + final Setting> oldSetting = + Setting.listSetting("foo.old", Collections.emptyList(), Function.identity(), Property.NodeScope); + final Setting> newSetting = + Setting.listSetting("foo.new", Collections.emptyList(), Function.identity(), Property.NodeScope); + + final AbstractScopedSettings service = + new ClusterSettings( + Settings.EMPTY, + new HashSet<>(Arrays.asList(oldSetting, newSetting)), + Collections.singleton(new SettingUpgrader>() { + + @Override + public Setting> getSetting() { + return oldSetting; + } + + @Override + public String getKey(final String key) { + return "foo.new"; + } + + @Override + public List getListValue(final List value) { + return value.stream().map(s -> "new." + s).collect(Collectors.toList()); + } + })); + + final int length = randomIntBetween(0, 16); + final List values = length == 0 ? Collections.emptyList() : new ArrayList<>(length); + for (int i = 0; i < length; i++) { + values.add(randomAlphaOfLength(8)); + } + + final Settings settings = Settings.builder().putList("foo.old", values).build(); + final Settings upgradedSettings = service.upgradeSettings(settings); + assertFalse(oldSetting.exists(upgradedSettings)); + assertTrue(newSetting.exists(upgradedSettings)); + assertThat( + newSetting.get(upgradedSettings), + equalTo(oldSetting.get(settings).stream().map(s -> "new." + s).collect(Collectors.toList()))); + } + } From 36bdad4895395e6854ffe2b783aec47ec93b4b1f Mon Sep 17 00:00:00 2001 From: Alan Woodward Date: Tue, 11 Sep 2018 13:38:44 +0100 Subject: [PATCH 08/78] Use IndexWriter.getFlushingBytes() rather than tracking it ourselves (#33582) Currently we keep track of how many bytes are currently being written to disk in an AtomicLong within InternalEngine, updating it on refresh. The IndexWriter has its own accounting for this, and exposes it via a getFlushingBytes method in the latest lucene 8 snapshot. This commit removes the InternalEngine tracking in favour of just using the IndexWriter method. --- .../elasticsearch/index/engine/InternalEngine.java | 13 +------------ .../elasticsearch/index/engine/LiveVersionMap.java | 8 ++++++++ .../index/engine/LiveVersionMapTests.java | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java index b2ab0d71c32..e8f5e415908 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java @@ -152,12 +152,6 @@ public class InternalEngine extends Engine { private final SoftDeletesPolicy softDeletesPolicy; private final LastRefreshedCheckpointListener lastRefreshedCheckpointListener; - /** - * How many bytes we are currently moving to disk, via either IndexWriter.flush or refresh. IndexingMemoryController polls this - * across all shards to decide if throttling is necessary because moving bytes to disk is falling behind vs incoming documents - * being indexed/deleted. - */ - private final AtomicLong writingBytes = new AtomicLong(); private final AtomicBoolean trackTranslogLocation = new AtomicBoolean(false); @Nullable @@ -530,7 +524,7 @@ public class InternalEngine extends Engine { /** Returns how many bytes we are currently moving from indexing buffer to segments on disk */ @Override public long getWritingBytes() { - return writingBytes.get(); + return indexWriter.getFlushingBytes() + versionMap.getRefreshingBytes(); } /** @@ -1437,9 +1431,6 @@ public class InternalEngine extends Engine { // pass the new reader reference to the external reader manager. final long localCheckpointBeforeRefresh = getLocalCheckpoint(); - // this will also cause version map ram to be freed hence we always account for it. - final long bytes = indexWriter.ramBytesUsed() + versionMap.ramBytesUsedForRefresh(); - writingBytes.addAndGet(bytes); try (ReleasableLock lock = readLock.acquire()) { ensureOpen(); if (store.tryIncRef()) { @@ -1465,8 +1456,6 @@ public class InternalEngine extends Engine { e.addSuppressed(inner); } throw new RefreshFailedEngineException(shardId, e); - } finally { - writingBytes.addAndGet(-bytes); } assert lastRefreshedCheckpoint() >= localCheckpointBeforeRefresh : "refresh checkpoint was not advanced; " + "local_checkpoint=" + localCheckpointBeforeRefresh + " refresh_checkpoint=" + lastRefreshedCheckpoint(); diff --git a/server/src/main/java/org/elasticsearch/index/engine/LiveVersionMap.java b/server/src/main/java/org/elasticsearch/index/engine/LiveVersionMap.java index 18d3cedb37e..d0dd9466b60 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/LiveVersionMap.java +++ b/server/src/main/java/org/elasticsearch/index/engine/LiveVersionMap.java @@ -434,6 +434,14 @@ final class LiveVersionMap implements ReferenceManager.RefreshListener, Accounta return maps.current.ramBytesUsed.get(); } + /** + * Returns how much RAM is current being freed up by refreshing. This is {@link #ramBytesUsed()} + * except does not include tombstones because they don't clear on refresh. + */ + long getRefreshingBytes() { + return maps.old.ramBytesUsed.get(); + } + @Override public Collection getChildResources() { // TODO: useful to break down RAM usage here? diff --git a/server/src/test/java/org/elasticsearch/index/engine/LiveVersionMapTests.java b/server/src/test/java/org/elasticsearch/index/engine/LiveVersionMapTests.java index 286e85cef3f..115785b2e7b 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/LiveVersionMapTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/LiveVersionMapTests.java @@ -41,6 +41,7 @@ import java.util.concurrent.atomic.AtomicLong; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.nullValue; public class LiveVersionMapTests extends ESTestCase { @@ -91,6 +92,19 @@ public class LiveVersionMapTests extends ESTestCase { assertEquals(actualRamBytesUsed, estimatedRamBytesUsed, tolerance); } + public void testRefreshingBytes() throws IOException { + LiveVersionMap map = new LiveVersionMap(); + BytesRefBuilder uid = new BytesRefBuilder(); + uid.copyChars(TestUtil.randomSimpleString(random(), 10, 20)); + try (Releasable r = map.acquireLock(uid.toBytesRef())) { + map.putIndexUnderLock(uid.toBytesRef(), randomIndexVersionValue()); + } + map.beforeRefresh(); + assertThat(map.getRefreshingBytes(), greaterThan(0L)); + map.afterRefresh(true); + assertThat(map.getRefreshingBytes(), equalTo(0L)); + } + private BytesRef uid(String string) { BytesRefBuilder builder = new BytesRefBuilder(); builder.copyChars(string); From 73c75bef216ee2d0658c7029c7c97e587983caad Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Tue, 11 Sep 2018 08:40:22 -0400 Subject: [PATCH 09/78] Preserve cluster settings on full restart tests (#33590) Today the full cluster restart tests do not preserve cluster settings on restart. This is a mistake because it is not an accurate reflection of reality, we do not expect users to clear cluster settings when they perform a full cluster restart. This commit makes it so that all full cluster restart tests preserve settings on upgrade. --- .../upgrades/AbstractFullClusterRestartTestCase.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/framework/src/main/java/org/elasticsearch/upgrades/AbstractFullClusterRestartTestCase.java b/test/framework/src/main/java/org/elasticsearch/upgrades/AbstractFullClusterRestartTestCase.java index 62c8e2f00ff..7e73e795b8a 100644 --- a/test/framework/src/main/java/org/elasticsearch/upgrades/AbstractFullClusterRestartTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/upgrades/AbstractFullClusterRestartTestCase.java @@ -57,4 +57,9 @@ public abstract class AbstractFullClusterRestartTestCase extends ESRestTestCase return true; } + @Override + protected boolean preserveClusterSettings() { + return true; + } + } From 624b84f897bff2fc2570dcba875b43f16d8d1321 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Tue, 11 Sep 2018 14:32:43 +0100 Subject: [PATCH 10/78] Improves doc values format deprecation message (#33576) * Improves doc values format deprecation message This changes the deprecation message when doc values fields do not supply a format form logging a deprecation warning for each offending field individually to logging a single message which lists all offending fields Closes #33572 * Updates YAML test with new deprecation message Also adds a test to ensure multiple deprecation warnings are collated into one message * Condenses collection of fields without format check Moves the collection of fields that don't have a format to a separate loop and moves the logging of the deprecation warning to be next to it at the expesnse of looping through the field list twice * fixes typo * Fixes test --- .../test/search/10_source_filtering.yml | 18 ++++++++++++++++-- .../subphase/DocValueFieldsFetchSubPhase.java | 14 ++++++++++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/search/10_source_filtering.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/search/10_source_filtering.yml index 59692873cc4..2725580d9e8 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/search/10_source_filtering.yml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/search/10_source_filtering.yml @@ -139,12 +139,26 @@ setup: features: warnings - do: warnings: - - 'Doc-value field [count] is not using a format. The output will change in 7.0 when doc value fields get formatted based on mappings by default. It is recommended to pass [format=use_field_mapping] with the doc value field in order to opt in for the future behaviour and ease the migration to 7.0.' + - 'There are doc-value fields which are not using a format. The output will change in 7.0 when doc value fields get formatted based on mappings by default. It is recommended to pass [format=use_field_mapping] with a doc value field in order to opt in for the future behaviour and ease the migration to 7.0: [count]' search: body: docvalue_fields: [ "count" ] - match: { hits.hits.0.fields.count: [1] } +--- +"multiple docvalue_fields": + - skip: + version: " - 6.3.99" + reason: format option was added in 6.4 + features: warnings + - do: + warnings: + - 'There are doc-value fields which are not using a format. The output will change in 7.0 when doc value fields get formatted based on mappings by default. It is recommended to pass [format=use_field_mapping] with a doc value field in order to opt in for the future behaviour and ease the migration to 7.0: [count, include.field1.keyword]' + search: + body: + docvalue_fields: [ "count", "include.field1.keyword" ] + - match: { hits.hits.0.fields.count: [1] } + --- "docvalue_fields as url param": - skip: @@ -153,7 +167,7 @@ setup: features: warnings - do: warnings: - - 'Doc-value field [count] is not using a format. The output will change in 7.0 when doc value fields get formatted based on mappings by default. It is recommended to pass [format=use_field_mapping] with the doc value field in order to opt in for the future behaviour and ease the migration to 7.0.' + - 'There are doc-value fields which are not using a format. The output will change in 7.0 when doc value fields get formatted based on mappings by default. It is recommended to pass [format=use_field_mapping] with a doc value field in order to opt in for the future behaviour and ease the migration to 7.0: [count]' search: docvalue_fields: [ "count" ] - match: { hits.hits.0.fields.count: [1] } diff --git a/server/src/main/java/org/elasticsearch/search/fetch/subphase/DocValueFieldsFetchSubPhase.java b/server/src/main/java/org/elasticsearch/search/fetch/subphase/DocValueFieldsFetchSubPhase.java index 3ef3064697a..97e5b70f9da 100644 --- a/server/src/main/java/org/elasticsearch/search/fetch/subphase/DocValueFieldsFetchSubPhase.java +++ b/server/src/main/java/org/elasticsearch/search/fetch/subphase/DocValueFieldsFetchSubPhase.java @@ -46,6 +46,7 @@ import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; /** * Query sub phase which pulls data from doc values @@ -77,6 +78,15 @@ public final class DocValueFieldsFetchSubPhase implements FetchSubPhase { hits = hits.clone(); // don't modify the incoming hits Arrays.sort(hits, Comparator.comparingInt(SearchHit::docId)); + List noFormatFields = context.docValueFieldsContext().fields().stream().filter(f -> f.format == null).map(f -> f.field) + .collect(Collectors.toList()); + if (noFormatFields.isEmpty() == false) { + DEPRECATION_LOGGER.deprecated("There are doc-value fields which are not using a format. The output will " + + "change in 7.0 when doc value fields get formatted based on mappings by default. It is recommended to pass " + + "[format={}] with a doc value field in order to opt in for the future behaviour and ease the migration to " + + "7.0: {}", DocValueFieldsContext.USE_DEFAULT_FORMAT, noFormatFields); + } + for (FieldAndFormat fieldAndFormat : context.docValueFieldsContext().fields()) { String field = fieldAndFormat.field; MappedFieldType fieldType = context.mapperService().fullName(field); @@ -84,10 +94,6 @@ public final class DocValueFieldsFetchSubPhase implements FetchSubPhase { final IndexFieldData indexFieldData = context.getForField(fieldType); final DocValueFormat format; if (fieldAndFormat.format == null) { - DEPRECATION_LOGGER.deprecated("Doc-value field [" + fieldAndFormat.field + "] is not using a format. The output will " + - "change in 7.0 when doc value fields get formatted based on mappings by default. It is recommended to pass " + - "[format={}] with the doc value field in order to opt in for the future behaviour and ease the migration to " + - "7.0.", DocValueFieldsContext.USE_DEFAULT_FORMAT); format = null; } else { String formatDesc = fieldAndFormat.format; From cbc6fa0ecb5cd6fe8695dee5ae93b2a5325e69c4 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 11 Sep 2018 07:56:26 -0700 Subject: [PATCH 11/78] [DOCS] Adds missing built-in user information (#33585) --- docs/reference/setup/install/windows.asciidoc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/reference/setup/install/windows.asciidoc b/docs/reference/setup/install/windows.asciidoc index f2e9077e20e..dffdc48fe7b 100644 --- a/docs/reference/setup/install/windows.asciidoc +++ b/docs/reference/setup/install/windows.asciidoc @@ -295,8 +295,9 @@ as _properties_ within Windows Installer documentation) that can be passed to `m `SKIPSETTINGPASSWORDS`:: - When installing with a `Trial` license and X-Pack Security enabled, whether the - installation should skip setting up the built-in users `elastic`, `kibana` and `logstash_system`. + When installing with a `Trial` license and {security} enabled, whether the + installation should skip setting up the built-in users `elastic`, `kibana`, + `logstash_system`, `apm_system`, and `beats_system`. Defaults to `false` `ELASTICUSERPASSWORD`:: From 91bca174f507b56e320ab2bd6bb0afc54e52e71b Mon Sep 17 00:00:00 2001 From: Costin Leau Date: Tue, 11 Sep 2018 18:47:49 +0300 Subject: [PATCH 12/78] SQL: Make Literal a NamedExpression (#33583) * SQL: Make Literal a NamedExpression Literal now is a NamedExpression reducing the need for Aliases for folded expressions leading to simpler optimization rules. Fix #33523 --- .../xpack/sql/expression/Attribute.java | 26 +- .../xpack/sql/expression/Expressions.java | 10 +- .../xpack/sql/expression/Literal.java | 68 ++++- .../sql/expression/LiteralAttribute.java | 10 +- .../function/scalar/ScalarFunction.java | 13 +- .../expression/function/scalar/math/E.java | 2 +- .../expression/function/scalar/math/Pi.java | 2 +- .../xpack/sql/optimizer/Optimizer.java | 41 +-- .../xpack/sql/expression/LiteralTests.java | 2 +- .../xpack/sql/optimizer/OptimizerTests.java | 262 +++++++++--------- 10 files changed, 214 insertions(+), 222 deletions(-) diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Attribute.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Attribute.java index 5143a7eceb4..dd18363b2a8 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Attribute.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Attribute.java @@ -7,30 +7,28 @@ package org.elasticsearch.xpack.sql.expression; import org.elasticsearch.xpack.sql.tree.Location; +import java.util.List; import java.util.Objects; import static java.util.Collections.emptyList; -import java.util.List; - /** - * {@link Expression}s that can be converted into Elasticsearch - * sorts, aggregations, or queries. They can also be extracted - * from the result of a search. + * {@link Expression}s that can be materialized and represent the result columns sent to the client. + * Typically are converted into constants, functions or Elasticsearch order-bys, + * aggregations, or queries. They can also be extracted from the result of a search. * * In the statement {@code SELECT ABS(foo), A, B+C FROM ...} the three named - * expressions (ABS(foo), A, B+C) get converted to attributes and the user can + * expressions {@code ABS(foo), A, B+C} get converted to attributes and the user can * only see Attributes. * - * In the statement {@code SELECT foo FROM TABLE WHERE foo > 10 + 1} 10+1 is an - * expression. It's not named - meaning there's no alias for it (defined by the - * user) and as such there's no attribute - no column to be returned to the user. - * It's an expression used for filtering so it doesn't appear in the result set - * (derived table). "foo" on the other hand is an expression, a named expression - * (it has a name) and also an attribute - it's a column in the result set. + * In the statement {@code SELECT foo FROM TABLE WHERE foo > 10 + 1} both {@code foo} and + * {@code 10 + 1} are named expressions, the first due to the SELECT, the second due to being a function. + * However since {@code 10 + 1} is used for filtering it doesn't appear appear in the result set + * (derived table) and as such it is never translated to an attribute. + * "foo" on the other hand is since it's a column in the result set. * - * Another example {@code SELECT foo FROM ... WHERE bar > 10 +1} "foo" gets - * converted into an Attribute, bar does not. That's because bar is used for + * Another example {@code SELECT foo FROM ... WHERE bar > 10 +1} {@code foo} gets + * converted into an Attribute, bar does not. That's because {@code bar} is used for * filtering alone but it's not part of the projection meaning the user doesn't * need it in the derived table. */ diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Expressions.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Expressions.java index 8ee34e32a55..1b326e0474f 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Expressions.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Expressions.java @@ -82,13 +82,7 @@ public abstract class Expressions { } public static String name(Expression e) { - if (e instanceof NamedExpression) { - return ((NamedExpression) e).name(); - } else if (e instanceof Literal) { - return e.toString(); - } else { - return e.nodeName(); - } + return e instanceof NamedExpression ? ((NamedExpression) e).name() : e.nodeName(); } public static List names(Collection e) { @@ -105,7 +99,7 @@ public abstract class Expressions { return ((NamedExpression) e).toAttribute(); } if (e != null && e.foldable()) { - return new LiteralAttribute(Literal.of(e)); + return Literal.of(e).toAttribute(); } return null; } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Literal.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Literal.java index 9a4ffce9295..4badfc7091c 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Literal.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Literal.java @@ -12,9 +12,16 @@ import org.elasticsearch.xpack.sql.type.DataType; import org.elasticsearch.xpack.sql.type.DataTypeConversion; import org.elasticsearch.xpack.sql.type.DataTypes; +import java.util.List; import java.util.Objects; -public class Literal extends LeafExpression { +import static java.util.Collections.emptyList; + +/** + * SQL Literal or constant. + */ +public class Literal extends NamedExpression { + public static final Literal TRUE = Literal.of(Location.EMPTY, Boolean.TRUE); public static final Literal FALSE = Literal.of(Location.EMPTY, Boolean.FALSE); @@ -22,7 +29,11 @@ public class Literal extends LeafExpression { private final DataType dataType; public Literal(Location location, Object value, DataType dataType) { - super(location); + this(location, null, value, dataType); + } + + public Literal(Location location, String name, Object value, DataType dataType) { + super(location, name == null ? String.valueOf(value) : name, emptyList(), null); this.dataType = dataType; this.value = DataTypeConversion.convert(value, dataType); } @@ -61,10 +72,24 @@ public class Literal extends LeafExpression { return value; } + @Override + public Attribute toAttribute() { + return new LiteralAttribute(location(), name(), null, false, id(), false, dataType, this); + } + + @Override + public Expression replaceChildren(List newChildren) { + throw new UnsupportedOperationException("this type of node doesn't have any children to replace"); + } + + @Override + public AttributeSet references() { + return AttributeSet.EMPTY; + } @Override public int hashCode() { - return Objects.hash(value, dataType); + return Objects.hash(name(), value, dataType); } @Override @@ -72,21 +97,25 @@ public class Literal extends LeafExpression { if (this == obj) { return true; } - if (obj == null || getClass() != obj.getClass()) { return false; } Literal other = (Literal) obj; - return Objects.equals(value, other.value) + return Objects.equals(name(), other.name()) + && Objects.equals(value, other.value) && Objects.equals(dataType, other.dataType); } @Override public String toString() { - return Objects.toString(value); + String s = String.valueOf(value); + return name().equals(s) ? s : name() + "=" + value; } + /** + * Utility method for creating 'in-line' Literals (out of values instead of expressions). + */ public static Literal of(Location loc, Object value) { if (value instanceof Literal) { return (Literal) value; @@ -94,15 +123,32 @@ public class Literal extends LeafExpression { return new Literal(loc, value, DataTypes.fromJava(value)); } + /** + * Utility method for creating a literal out of a foldable expression. + * Throws an exception if the expression is not foldable. + */ public static Literal of(Expression foldable) { - if (foldable instanceof Literal) { - return (Literal) foldable; - } + return of((String) null, foldable); + } + public static Literal of(String name, Expression foldable) { if (!foldable.foldable()) { throw new SqlIllegalArgumentException("Foldable expression required for Literal creation; received unfoldable " + foldable); } - return new Literal(foldable.location(), foldable.fold(), foldable.dataType()); + if (foldable instanceof Literal) { + Literal l = (Literal) foldable; + if (name == null || l.name().equals(name)) { + return l; + } + } + + Object fold = foldable.fold(); + + if (name == null) { + name = foldable instanceof NamedExpression ? ((NamedExpression) foldable).name() : String.valueOf(fold); + } + + return new Literal(foldable.location(), name, fold, foldable.dataType()); } -} +} \ No newline at end of file diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/LiteralAttribute.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/LiteralAttribute.java index ff07731b82e..a6483458a6b 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/LiteralAttribute.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/LiteralAttribute.java @@ -15,20 +15,12 @@ public class LiteralAttribute extends TypedAttribute { private final Literal literal; - public LiteralAttribute(Literal literal) { - this(literal.location(), String.valueOf(literal.fold()), null, false, null, false, literal.dataType(), literal); - } - public LiteralAttribute(Location location, String name, String qualifier, boolean nullable, ExpressionId id, boolean synthetic, DataType dataType, Literal literal) { super(location, name, dataType, qualifier, nullable, id, synthetic); this.literal = literal; } - public Literal literal() { - return literal; - } - @Override protected NodeInfo info() { return NodeInfo.create(this, LiteralAttribute::new, @@ -49,4 +41,4 @@ public class LiteralAttribute extends TypedAttribute { protected String label() { return "c"; } -} +} \ No newline at end of file diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/ScalarFunction.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/ScalarFunction.java index 309ee4e8e86..e7b8529557f 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/ScalarFunction.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/ScalarFunction.java @@ -10,7 +10,6 @@ import org.elasticsearch.xpack.sql.expression.Attribute; import org.elasticsearch.xpack.sql.expression.Expression; import org.elasticsearch.xpack.sql.expression.Expressions; import org.elasticsearch.xpack.sql.expression.FieldAttribute; -import org.elasticsearch.xpack.sql.expression.LiteralAttribute; import org.elasticsearch.xpack.sql.expression.function.Function; import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunctionAttribute; import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; @@ -69,11 +68,9 @@ public abstract class ScalarFunction extends Function { if (attr instanceof AggregateFunctionAttribute) { return asScriptFrom((AggregateFunctionAttribute) attr); } - if (attr instanceof LiteralAttribute) { - return asScriptFrom((LiteralAttribute) attr); + if (attr instanceof FieldAttribute) { + return asScriptFrom((FieldAttribute) attr); } - // fall-back to - return asScriptFrom((FieldAttribute) attr); } throw new SqlIllegalArgumentException("Cannot evaluate script for expression {}", exp); } @@ -102,12 +99,6 @@ public abstract class ScalarFunction extends Function { aggregate.dataType()); } - protected ScriptTemplate asScriptFrom(LiteralAttribute literal) { - return new ScriptTemplate(formatScript("{}"), - paramsBuilder().variable(literal.literal()).build(), - literal.dataType()); - } - protected String formatScript(String scriptTemplate) { return formatTemplate(scriptTemplate); } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/E.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/E.java index 921b6edaef6..a3fdfa654df 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/E.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/E.java @@ -21,7 +21,7 @@ public class E extends MathFunction { private static final ScriptTemplate TEMPLATE = new ScriptTemplate("Math.E", Params.EMPTY, DataType.DOUBLE); public E(Location location) { - super(location, new Literal(location, Math.E, DataType.DOUBLE)); + super(location, new Literal(location, "E", Math.E, DataType.DOUBLE)); } @Override diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/Pi.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/Pi.java index 9758843ee5d..e57aa333f06 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/Pi.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/math/Pi.java @@ -21,7 +21,7 @@ public class Pi extends MathFunction { private static final ScriptTemplate TEMPLATE = new ScriptTemplate("Math.PI", Params.EMPTY, DataType.DOUBLE); public Pi(Location location) { - super(location, new Literal(location, Math.PI, DataType.DOUBLE)); + super(location, new Literal(location, "PI", Math.PI, DataType.DOUBLE)); } @Override diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/optimizer/Optimizer.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/optimizer/Optimizer.java index 55c4112d38b..72105a2fae8 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/optimizer/Optimizer.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/optimizer/Optimizer.java @@ -1118,36 +1118,12 @@ public class Optimizer extends RuleExecutor { @Override protected Expression rule(Expression e) { - // handle aliases to avoid double aliasing of functions - // alias points to function which gets folded and wrapped in an alias that is - // aliases if (e instanceof Alias) { Alias a = (Alias) e; - Expression fold = fold(a.child()); - if (fold != a.child()) { - return new Alias(a.location(), a.name(), null, fold, a.id()); - } - return a; + return a.child().foldable() ? Literal.of(a.name(), a.child()) : a; } - Expression fold = fold(e); - if (fold != e) { - // preserve the name through an alias - if (e instanceof NamedExpression) { - NamedExpression ne = (NamedExpression) e; - return new Alias(e.location(), ne.name(), null, fold, ne.id()); - } - return fold; - } - return e; - } - - private Expression fold(Expression e) { - // literals are always foldable, so avoid creating a duplicate - if (e.foldable() && !(e instanceof Literal)) { - return new Literal(e.location(), e.fold(), e.dataType()); - } - return e; + return e.foldable() ? Literal.of(e) : e; } } @@ -1836,14 +1812,11 @@ public class Optimizer extends RuleExecutor { private List extractConstants(List named) { List values = new ArrayList<>(); for (NamedExpression n : named) { - if (n instanceof Alias) { - Alias a = (Alias) n; - if (a.child().foldable()) { - values.add(a.child().fold()); - } - else { - return values; - } + if (n.foldable()) { + values.add(n.fold()); + } else { + // not everything is foldable, bail-out early + return values; } } return values; diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/LiteralTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/LiteralTests.java index 8527c5b62df..d6bd6ab96b2 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/LiteralTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/LiteralTests.java @@ -61,7 +61,7 @@ public class LiteralTests extends AbstractNodeTestCase { @Override protected Literal copy(Literal instance) { - return new Literal(instance.location(), instance.value(), instance.dataType()); + return new Literal(instance.location(), instance.name(), instance.value(), instance.dataType()); } @Override diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/optimizer/OptimizerTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/optimizer/OptimizerTests.java index ed4e54701dc..07349008c07 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/optimizer/OptimizerTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/optimizer/OptimizerTests.java @@ -85,6 +85,14 @@ public class OptimizerTests extends ESTestCase { private static final Expression DUMMY_EXPRESSION = new DummyBooleanExpression(EMPTY, 0); + private static final Literal ONE = L(1); + private static final Literal TWO = L(2); + private static final Literal THREE = L(3); + private static final Literal FOUR = L(4); + private static final Literal FIVE = L(5); + private static final Literal SIX = L(6); + + public static class DummyBooleanExpression extends Expression { private final int id; @@ -161,7 +169,7 @@ public class OptimizerTests extends ESTestCase { public void testCombineProjections() { // a - Alias a = new Alias(EMPTY, "a", L(5)); + Alias a = new Alias(EMPTY, "a", FIVE); // b Alias b = new Alias(EMPTY, "b", L(10)); // x -> a @@ -187,7 +195,7 @@ public class OptimizerTests extends ESTestCase { // SELECT 5 a, 10 b FROM foo WHERE a < 10 ORDER BY b // a - Alias a = new Alias(EMPTY, "a", L(5)); + Alias a = new Alias(EMPTY, "a", FIVE); // b Alias b = new Alias(EMPTY, "b", L(10)); // WHERE a < 10 @@ -226,49 +234,44 @@ public class OptimizerTests extends ESTestCase { // public void testConstantFolding() { - Expression exp = new Add(EMPTY, L(2), L(3)); + Expression exp = new Add(EMPTY, TWO, THREE); assertTrue(exp.foldable()); assertTrue(exp instanceof NamedExpression); String n = Expressions.name(exp); Expression result = new ConstantFolding().rule(exp); - assertTrue(result instanceof Alias); + assertTrue(result instanceof Literal); assertEquals(n, Expressions.name(result)); - Expression c = ((Alias) result).child(); - assertTrue(c instanceof Literal); - assertEquals(5, ((Literal) c).value()); + assertEquals(5, ((Literal) result).value()); // check now with an alias result = new ConstantFolding().rule(new Alias(EMPTY, "a", exp)); - assertTrue(result instanceof Alias); assertEquals("a", Expressions.name(result)); - c = ((Alias) result).child(); - assertTrue(c instanceof Literal); - assertEquals(5, ((Literal) c).value()); + assertEquals(5, ((Literal) result).value()); } public void testConstantFoldingBinaryComparison() { - assertEquals(Literal.FALSE, new ConstantFolding().rule(new GreaterThan(EMPTY, L(2), L(3)))); - assertEquals(Literal.FALSE, new ConstantFolding().rule(new GreaterThanOrEqual(EMPTY, L(2), L(3)))); - assertEquals(Literal.FALSE, new ConstantFolding().rule(new Equals(EMPTY, L(2), L(3)))); - assertEquals(Literal.TRUE, new ConstantFolding().rule(new LessThanOrEqual(EMPTY, L(2), L(3)))); - assertEquals(Literal.TRUE, new ConstantFolding().rule(new LessThan(EMPTY, L(2), L(3)))); + assertEquals(Literal.FALSE, new ConstantFolding().rule(new GreaterThan(EMPTY, TWO, THREE))); + assertEquals(Literal.FALSE, new ConstantFolding().rule(new GreaterThanOrEqual(EMPTY, TWO, THREE))); + assertEquals(Literal.FALSE, new ConstantFolding().rule(new Equals(EMPTY, TWO, THREE))); + assertEquals(Literal.TRUE, new ConstantFolding().rule(new LessThanOrEqual(EMPTY, TWO, THREE))); + assertEquals(Literal.TRUE, new ConstantFolding().rule(new LessThan(EMPTY, TWO, THREE))); } public void testConstantFoldingBinaryLogic() { - assertEquals(Literal.FALSE, new ConstantFolding().rule(new And(EMPTY, new GreaterThan(EMPTY, L(2), L(3)), Literal.TRUE))); - assertEquals(Literal.TRUE, new ConstantFolding().rule(new Or(EMPTY, new GreaterThanOrEqual(EMPTY, L(2), L(3)), Literal.TRUE))); + assertEquals(Literal.FALSE, new ConstantFolding().rule(new And(EMPTY, new GreaterThan(EMPTY, TWO, THREE), Literal.TRUE))); + assertEquals(Literal.TRUE, new ConstantFolding().rule(new Or(EMPTY, new GreaterThanOrEqual(EMPTY, TWO, THREE), Literal.TRUE))); } public void testConstantFoldingRange() { - assertEquals(Literal.TRUE, new ConstantFolding().rule(new Range(EMPTY, L(5), L(5), true, L(10), false))); - assertEquals(Literal.FALSE, new ConstantFolding().rule(new Range(EMPTY, L(5), L(5), false, L(10), false))); + assertEquals(Literal.TRUE, new ConstantFolding().rule(new Range(EMPTY, FIVE, FIVE, true, L(10), false))); + assertEquals(Literal.FALSE, new ConstantFolding().rule(new Range(EMPTY, FIVE, FIVE, false, L(10), false))); } public void testConstantIsNotNull() { assertEquals(Literal.FALSE, new ConstantFolding().rule(new IsNotNull(EMPTY, L(null)))); - assertEquals(Literal.TRUE, new ConstantFolding().rule(new IsNotNull(EMPTY, L(5)))); + assertEquals(Literal.TRUE, new ConstantFolding().rule(new IsNotNull(EMPTY, FIVE))); } public void testConstantNot() { @@ -296,30 +299,24 @@ public class OptimizerTests extends ESTestCase { } public void testArithmeticFolding() { - assertEquals(10, foldFunction(new Add(EMPTY, L(7), L(3)))); - assertEquals(4, foldFunction(new Sub(EMPTY, L(7), L(3)))); - assertEquals(21, foldFunction(new Mul(EMPTY, L(7), L(3)))); - assertEquals(2, foldFunction(new Div(EMPTY, L(7), L(3)))); - assertEquals(1, foldFunction(new Mod(EMPTY, L(7), L(3)))); + assertEquals(10, foldFunction(new Add(EMPTY, L(7), THREE))); + assertEquals(4, foldFunction(new Sub(EMPTY, L(7), THREE))); + assertEquals(21, foldFunction(new Mul(EMPTY, L(7), THREE))); + assertEquals(2, foldFunction(new Div(EMPTY, L(7), THREE))); + assertEquals(1, foldFunction(new Mod(EMPTY, L(7), THREE))); } public void testMathFolding() { assertEquals(7, foldFunction(new Abs(EMPTY, L(7)))); - assertEquals(0d, (double) foldFunction(new ACos(EMPTY, L(1))), 0.01d); - assertEquals(1.57076d, (double) foldFunction(new ASin(EMPTY, L(1))), 0.01d); - assertEquals(0.78539d, (double) foldFunction(new ATan(EMPTY, L(1))), 0.01d); + assertEquals(0d, (double) foldFunction(new ACos(EMPTY, ONE)), 0.01d); + assertEquals(1.57076d, (double) foldFunction(new ASin(EMPTY, ONE)), 0.01d); + assertEquals(0.78539d, (double) foldFunction(new ATan(EMPTY, ONE)), 0.01d); assertEquals(7, foldFunction(new Floor(EMPTY, L(7)))); assertEquals(Math.E, foldFunction(new E(EMPTY))); } private static Object foldFunction(Function f) { - return unwrapAlias(new ConstantFolding().rule(f)); - } - - private static Object unwrapAlias(Expression e) { - Alias a = (Alias) e; - Literal l = (Literal) a.child(); - return l.value(); + return ((Literal) new ConstantFolding().rule(f)).value(); } // @@ -327,21 +324,21 @@ public class OptimizerTests extends ESTestCase { // public void testBinaryComparisonSimplification() { - assertEquals(Literal.TRUE, new BinaryComparisonSimplification().rule(new Equals(EMPTY, L(5), L(5)))); - assertEquals(Literal.TRUE, new BinaryComparisonSimplification().rule(new GreaterThanOrEqual(EMPTY, L(5), L(5)))); - assertEquals(Literal.TRUE, new BinaryComparisonSimplification().rule(new LessThanOrEqual(EMPTY, L(5), L(5)))); + assertEquals(Literal.TRUE, new BinaryComparisonSimplification().rule(new Equals(EMPTY, FIVE, FIVE))); + assertEquals(Literal.TRUE, new BinaryComparisonSimplification().rule(new GreaterThanOrEqual(EMPTY, FIVE, FIVE))); + assertEquals(Literal.TRUE, new BinaryComparisonSimplification().rule(new LessThanOrEqual(EMPTY, FIVE, FIVE))); - assertEquals(Literal.FALSE, new BinaryComparisonSimplification().rule(new GreaterThan(EMPTY, L(5), L(5)))); - assertEquals(Literal.FALSE, new BinaryComparisonSimplification().rule(new LessThan(EMPTY, L(5), L(5)))); + assertEquals(Literal.FALSE, new BinaryComparisonSimplification().rule(new GreaterThan(EMPTY, FIVE, FIVE))); + assertEquals(Literal.FALSE, new BinaryComparisonSimplification().rule(new LessThan(EMPTY, FIVE, FIVE))); } public void testLiteralsOnTheRight() { Alias a = new Alias(EMPTY, "a", L(10)); - Expression result = new BooleanLiteralsOnTheRight().rule(new Equals(EMPTY, L(5), a)); + Expression result = new BooleanLiteralsOnTheRight().rule(new Equals(EMPTY, FIVE, a)); assertTrue(result instanceof Equals); Equals eq = (Equals) result; assertEquals(a, eq.left()); - assertEquals(L(5), eq.right()); + assertEquals(FIVE, eq.right()); } public void testBoolSimplifyOr() { @@ -390,7 +387,7 @@ public class OptimizerTests extends ESTestCase { public void testFoldExcludingRangeToFalse() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - Range r = new Range(EMPTY, fa, L(6), false, L(5), true); + Range r = new Range(EMPTY, fa, SIX, false, FIVE, true); assertTrue(r.foldable()); assertEquals(Boolean.FALSE, r.fold()); } @@ -399,7 +396,7 @@ public class OptimizerTests extends ESTestCase { public void testFoldExcludingRangeWithDifferentTypesToFalse() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - Range r = new Range(EMPTY, fa, L(6), false, L(5.5d), true); + Range r = new Range(EMPTY, fa, SIX, false, L(5.5d), true); assertTrue(r.foldable()); assertEquals(Boolean.FALSE, r.fold()); } @@ -408,7 +405,7 @@ public class OptimizerTests extends ESTestCase { public void testCombineBinaryComparisonsNotComparable() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - LessThanOrEqual lte = new LessThanOrEqual(EMPTY, fa, L(6)); + LessThanOrEqual lte = new LessThanOrEqual(EMPTY, fa, SIX); LessThan lt = new LessThan(EMPTY, fa, Literal.FALSE); CombineBinaryComparisons rule = new CombineBinaryComparisons(); @@ -420,71 +417,71 @@ public class OptimizerTests extends ESTestCase { // a <= 6 AND a < 5 -> a < 5 public void testCombineBinaryComparisonsUpper() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - LessThanOrEqual lte = new LessThanOrEqual(EMPTY, fa, L(6)); - LessThan lt = new LessThan(EMPTY, fa, L(5)); + LessThanOrEqual lte = new LessThanOrEqual(EMPTY, fa, SIX); + LessThan lt = new LessThan(EMPTY, fa, FIVE); CombineBinaryComparisons rule = new CombineBinaryComparisons(); Expression exp = rule.rule(new And(EMPTY, lte, lt)); assertEquals(LessThan.class, exp.getClass()); LessThan r = (LessThan) exp; - assertEquals(L(5), r.right()); + assertEquals(FIVE, r.right()); } // 6 <= a AND 5 < a -> 6 <= a public void testCombineBinaryComparisonsLower() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - GreaterThanOrEqual gte = new GreaterThanOrEqual(EMPTY, fa, L(6)); - GreaterThan gt = new GreaterThan(EMPTY, fa, L(5)); + GreaterThanOrEqual gte = new GreaterThanOrEqual(EMPTY, fa, SIX); + GreaterThan gt = new GreaterThan(EMPTY, fa, FIVE); CombineBinaryComparisons rule = new CombineBinaryComparisons(); Expression exp = rule.rule(new And(EMPTY, gte, gt)); assertEquals(GreaterThanOrEqual.class, exp.getClass()); GreaterThanOrEqual r = (GreaterThanOrEqual) exp; - assertEquals(L(6), r.right()); + assertEquals(SIX, r.right()); } // 5 <= a AND 5 < a -> 5 < a public void testCombineBinaryComparisonsInclude() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - GreaterThanOrEqual gte = new GreaterThanOrEqual(EMPTY, fa, L(5)); - GreaterThan gt = new GreaterThan(EMPTY, fa, L(5)); + GreaterThanOrEqual gte = new GreaterThanOrEqual(EMPTY, fa, FIVE); + GreaterThan gt = new GreaterThan(EMPTY, fa, FIVE); CombineBinaryComparisons rule = new CombineBinaryComparisons(); Expression exp = rule.rule(new And(EMPTY, gte, gt)); assertEquals(GreaterThan.class, exp.getClass()); GreaterThan r = (GreaterThan) exp; - assertEquals(L(5), r.right()); + assertEquals(FIVE, r.right()); } // 3 <= a AND 4 < a AND a <= 7 AND a < 6 -> 4 < a < 6 public void testCombineMultipleBinaryComparisons() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - GreaterThanOrEqual gte = new GreaterThanOrEqual(EMPTY, fa, L(3)); - GreaterThan gt = new GreaterThan(EMPTY, fa, L(4)); + GreaterThanOrEqual gte = new GreaterThanOrEqual(EMPTY, fa, THREE); + GreaterThan gt = new GreaterThan(EMPTY, fa, FOUR); LessThanOrEqual lte = new LessThanOrEqual(EMPTY, fa, L(7)); - LessThan lt = new LessThan(EMPTY, fa, L(6)); + LessThan lt = new LessThan(EMPTY, fa, SIX); CombineBinaryComparisons rule = new CombineBinaryComparisons(); Expression exp = rule.rule(new And(EMPTY, gte, new And(EMPTY, gt, new And(EMPTY, lt, lte)))); assertEquals(Range.class, exp.getClass()); Range r = (Range) exp; - assertEquals(L(4), r.lower()); + assertEquals(FOUR, r.lower()); assertFalse(r.includeLower()); - assertEquals(L(6), r.upper()); + assertEquals(SIX, r.upper()); assertFalse(r.includeUpper()); } // 3 <= a AND TRUE AND 4 < a AND a != 5 AND a <= 7 -> 4 < a <= 7 AND a != 5 AND TRUE public void testCombineMixedMultipleBinaryComparisons() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - GreaterThanOrEqual gte = new GreaterThanOrEqual(EMPTY, fa, L(3)); - GreaterThan gt = new GreaterThan(EMPTY, fa, L(4)); + GreaterThanOrEqual gte = new GreaterThanOrEqual(EMPTY, fa, THREE); + GreaterThan gt = new GreaterThan(EMPTY, fa, FOUR); LessThanOrEqual lte = new LessThanOrEqual(EMPTY, fa, L(7)); - Expression ne = new Not(EMPTY, new Equals(EMPTY, fa, L(5))); + Expression ne = new Not(EMPTY, new Equals(EMPTY, fa, FIVE)); CombineBinaryComparisons rule = new CombineBinaryComparisons(); @@ -494,7 +491,7 @@ public class OptimizerTests extends ESTestCase { And and = ((And) exp); assertEquals(Range.class, and.right().getClass()); Range r = (Range) and.right(); - assertEquals(L(4), r.lower()); + assertEquals(FOUR, r.lower()); assertFalse(r.includeLower()); assertEquals(L(7), r.upper()); assertTrue(r.includeUpper()); @@ -503,17 +500,17 @@ public class OptimizerTests extends ESTestCase { // 1 <= a AND a < 5 -> 1 <= a < 5 public void testCombineComparisonsIntoRange() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - GreaterThanOrEqual gte = new GreaterThanOrEqual(EMPTY, fa, L(1)); - LessThan lt = new LessThan(EMPTY, fa, L(5)); + GreaterThanOrEqual gte = new GreaterThanOrEqual(EMPTY, fa, ONE); + LessThan lt = new LessThan(EMPTY, fa, FIVE); CombineBinaryComparisons rule = new CombineBinaryComparisons(); Expression exp = rule.rule(new And(EMPTY, gte, lt)); assertEquals(Range.class, rule.rule(exp).getClass()); Range r = (Range) exp; - assertEquals(L(1), r.lower()); + assertEquals(ONE, r.lower()); assertTrue(r.includeLower()); - assertEquals(L(5), r.upper()); + assertEquals(FIVE, r.upper()); assertFalse(r.includeUpper()); } @@ -521,10 +518,10 @@ public class OptimizerTests extends ESTestCase { public void testCombineUnbalancedComparisonsMixedWithEqualsIntoRange() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); IsNotNull isn = new IsNotNull(EMPTY, fa); - GreaterThanOrEqual gte = new GreaterThanOrEqual(EMPTY, fa, L(1)); + GreaterThanOrEqual gte = new GreaterThanOrEqual(EMPTY, fa, ONE); Equals eq = new Equals(EMPTY, fa, L(10)); - LessThan lt = new LessThan(EMPTY, fa, L(5)); + LessThan lt = new LessThan(EMPTY, fa, FIVE); And and = new And(EMPTY, new And(EMPTY, isn, gte), new And(EMPTY, lt, eq)); @@ -535,9 +532,9 @@ public class OptimizerTests extends ESTestCase { assertEquals(Range.class, a.right().getClass()); Range r = (Range) a.right(); - assertEquals(L(1), r.lower()); + assertEquals(ONE, r.lower()); assertTrue(r.includeLower()); - assertEquals(L(5), r.upper()); + assertEquals(FIVE, r.upper()); assertFalse(r.includeUpper()); } @@ -545,8 +542,8 @@ public class OptimizerTests extends ESTestCase { public void testCombineBinaryComparisonsConjunctionOfIncludedRange() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - Range r1 = new Range(EMPTY, fa, L(2), false, L(3), false); - Range r2 = new Range(EMPTY, fa, L(1), false, L(4), false); + Range r1 = new Range(EMPTY, fa, TWO, false, THREE, false); + Range r2 = new Range(EMPTY, fa, ONE, false, FOUR, false); And and = new And(EMPTY, r1, r2); @@ -559,8 +556,8 @@ public class OptimizerTests extends ESTestCase { public void testCombineBinaryComparisonsConjunctionOfNonOverlappingBoundaries() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - Range r1 = new Range(EMPTY, fa, L(2), false, L(3), false); - Range r2 = new Range(EMPTY, fa, L(1), false, L(2), false); + Range r1 = new Range(EMPTY, fa, TWO, false, THREE, false); + Range r2 = new Range(EMPTY, fa, ONE, false, TWO, false); And and = new And(EMPTY, r1, r2); @@ -568,9 +565,9 @@ public class OptimizerTests extends ESTestCase { Expression exp = rule.rule(and); assertEquals(Range.class, exp.getClass()); Range r = (Range) exp; - assertEquals(L(2), r.lower()); + assertEquals(TWO, r.lower()); assertFalse(r.includeLower()); - assertEquals(L(2), r.upper()); + assertEquals(TWO, r.upper()); assertFalse(r.includeUpper()); assertEquals(Boolean.FALSE, r.fold()); } @@ -579,8 +576,8 @@ public class OptimizerTests extends ESTestCase { public void testCombineBinaryComparisonsConjunctionOfUpperEqualsOverlappingBoundaries() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - Range r1 = new Range(EMPTY, fa, L(2), false, L(3), false); - Range r2 = new Range(EMPTY, fa, L(2), false, L(3), true); + Range r1 = new Range(EMPTY, fa, TWO, false, THREE, false); + Range r2 = new Range(EMPTY, fa, TWO, false, THREE, true); And and = new And(EMPTY, r1, r2); @@ -593,8 +590,8 @@ public class OptimizerTests extends ESTestCase { public void testCombineBinaryComparisonsConjunctionOverlappingUpperBoundary() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - Range r2 = new Range(EMPTY, fa, L(2), false, L(3), false); - Range r1 = new Range(EMPTY, fa, L(1), false, L(3), false); + Range r2 = new Range(EMPTY, fa, TWO, false, THREE, false); + Range r1 = new Range(EMPTY, fa, ONE, false, THREE, false); And and = new And(EMPTY, r1, r2); @@ -607,8 +604,8 @@ public class OptimizerTests extends ESTestCase { public void testCombineBinaryComparisonsConjunctionWithDifferentUpperLimitInclusion() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - Range r1 = new Range(EMPTY, fa, L(1), false, L(3), false); - Range r2 = new Range(EMPTY, fa, L(2), false, L(3), true); + Range r1 = new Range(EMPTY, fa, ONE, false, THREE, false); + Range r2 = new Range(EMPTY, fa, TWO, false, THREE, true); And and = new And(EMPTY, r1, r2); @@ -616,9 +613,9 @@ public class OptimizerTests extends ESTestCase { Expression exp = rule.rule(and); assertEquals(Range.class, exp.getClass()); Range r = (Range) exp; - assertEquals(L(2), r.lower()); + assertEquals(TWO, r.lower()); assertFalse(r.includeLower()); - assertEquals(L(3), r.upper()); + assertEquals(THREE, r.upper()); assertFalse(r.includeUpper()); } @@ -626,8 +623,8 @@ public class OptimizerTests extends ESTestCase { public void testRangesOverlappingConjunctionNoLowerBoundary() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - Range r1 = new Range(EMPTY, fa, L(0), false, L(1), true); - Range r2 = new Range(EMPTY, fa, L(0), true, L(2), false); + Range r1 = new Range(EMPTY, fa, L(0), false, ONE, true); + Range r2 = new Range(EMPTY, fa, L(0), true, TWO, false); And and = new And(EMPTY, r1, r2); @@ -641,7 +638,7 @@ public class OptimizerTests extends ESTestCase { public void testCombineBinaryComparisonsDisjunctionNotComparable() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - GreaterThan gt1 = new GreaterThan(EMPTY, fa, L(1)); + GreaterThan gt1 = new GreaterThan(EMPTY, fa, ONE); GreaterThan gt2 = new GreaterThan(EMPTY, fa, Literal.FALSE); Or or = new Or(EMPTY, gt1, gt2); @@ -656,9 +653,9 @@ public class OptimizerTests extends ESTestCase { public void testCombineBinaryComparisonsDisjunctionLowerBound() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - GreaterThan gt1 = new GreaterThan(EMPTY, fa, L(1)); - GreaterThan gt2 = new GreaterThan(EMPTY, fa, L(2)); - GreaterThan gt3 = new GreaterThan(EMPTY, fa, L(3)); + GreaterThan gt1 = new GreaterThan(EMPTY, fa, ONE); + GreaterThan gt2 = new GreaterThan(EMPTY, fa, TWO); + GreaterThan gt3 = new GreaterThan(EMPTY, fa, THREE); Or or = new Or(EMPTY, gt1, new Or(EMPTY, gt2, gt3)); @@ -667,16 +664,16 @@ public class OptimizerTests extends ESTestCase { assertEquals(GreaterThan.class, exp.getClass()); GreaterThan gt = (GreaterThan) exp; - assertEquals(L(1), gt.right()); + assertEquals(ONE, gt.right()); } // 2 < a OR 1 < a OR 3 <= a -> 1 < a public void testCombineBinaryComparisonsDisjunctionIncludeLowerBounds() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - GreaterThan gt1 = new GreaterThan(EMPTY, fa, L(1)); - GreaterThan gt2 = new GreaterThan(EMPTY, fa, L(2)); - GreaterThanOrEqual gte3 = new GreaterThanOrEqual(EMPTY, fa, L(3)); + GreaterThan gt1 = new GreaterThan(EMPTY, fa, ONE); + GreaterThan gt2 = new GreaterThan(EMPTY, fa, TWO); + GreaterThanOrEqual gte3 = new GreaterThanOrEqual(EMPTY, fa, THREE); Or or = new Or(EMPTY, new Or(EMPTY, gt1, gt2), gte3); @@ -685,16 +682,16 @@ public class OptimizerTests extends ESTestCase { assertEquals(GreaterThan.class, exp.getClass()); GreaterThan gt = (GreaterThan) exp; - assertEquals(L(1), gt.right()); + assertEquals(ONE, gt.right()); } // a < 1 OR a < 2 OR a < 3 -> a < 3 public void testCombineBinaryComparisonsDisjunctionUpperBound() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - LessThan lt1 = new LessThan(EMPTY, fa, L(1)); - LessThan lt2 = new LessThan(EMPTY, fa, L(2)); - LessThan lt3 = new LessThan(EMPTY, fa, L(3)); + LessThan lt1 = new LessThan(EMPTY, fa, ONE); + LessThan lt2 = new LessThan(EMPTY, fa, TWO); + LessThan lt3 = new LessThan(EMPTY, fa, THREE); Or or = new Or(EMPTY, new Or(EMPTY, lt1, lt2), lt3); @@ -703,16 +700,16 @@ public class OptimizerTests extends ESTestCase { assertEquals(LessThan.class, exp.getClass()); LessThan lt = (LessThan) exp; - assertEquals(L(3), lt.right()); + assertEquals(THREE, lt.right()); } // a < 2 OR a <= 2 OR a < 1 -> a <= 2 public void testCombineBinaryComparisonsDisjunctionIncludeUpperBounds() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - LessThan lt1 = new LessThan(EMPTY, fa, L(1)); - LessThan lt2 = new LessThan(EMPTY, fa, L(2)); - LessThanOrEqual lte2 = new LessThanOrEqual(EMPTY, fa, L(2)); + LessThan lt1 = new LessThan(EMPTY, fa, ONE); + LessThan lt2 = new LessThan(EMPTY, fa, TWO); + LessThanOrEqual lte2 = new LessThanOrEqual(EMPTY, fa, TWO); Or or = new Or(EMPTY, lt2, new Or(EMPTY, lte2, lt1)); @@ -721,18 +718,18 @@ public class OptimizerTests extends ESTestCase { assertEquals(LessThanOrEqual.class, exp.getClass()); LessThanOrEqual lte = (LessThanOrEqual) exp; - assertEquals(L(2), lte.right()); + assertEquals(TWO, lte.right()); } // a < 2 OR 3 < a OR a < 1 OR 4 < a -> a < 2 OR 3 < a public void testCombineBinaryComparisonsDisjunctionOfLowerAndUpperBounds() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - LessThan lt1 = new LessThan(EMPTY, fa, L(1)); - LessThan lt2 = new LessThan(EMPTY, fa, L(2)); + LessThan lt1 = new LessThan(EMPTY, fa, ONE); + LessThan lt2 = new LessThan(EMPTY, fa, TWO); - GreaterThan gt3 = new GreaterThan(EMPTY, fa, L(3)); - GreaterThan gt4 = new GreaterThan(EMPTY, fa, L(4)); + GreaterThan gt3 = new GreaterThan(EMPTY, fa, THREE); + GreaterThan gt4 = new GreaterThan(EMPTY, fa, FOUR); Or or = new Or(EMPTY, new Or(EMPTY, lt2, gt3), new Or(EMPTY, lt1, gt4)); @@ -744,18 +741,18 @@ public class OptimizerTests extends ESTestCase { assertEquals(LessThan.class, ro.left().getClass()); LessThan lt = (LessThan) ro.left(); - assertEquals(L(2), lt.right()); + assertEquals(TWO, lt.right()); assertEquals(GreaterThan.class, ro.right().getClass()); GreaterThan gt = (GreaterThan) ro.right(); - assertEquals(L(3), gt.right()); + assertEquals(THREE, gt.right()); } // (2 < a < 3) OR (1 < a < 4) -> (1 < a < 4) public void testCombineBinaryComparisonsDisjunctionOfIncludedRangeNotComparable() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - Range r1 = new Range(EMPTY, fa, L(2), false, L(3), false); - Range r2 = new Range(EMPTY, fa, L(1), false, Literal.FALSE, false); + Range r1 = new Range(EMPTY, fa, TWO, false, THREE, false); + Range r2 = new Range(EMPTY, fa, ONE, false, Literal.FALSE, false); Or or = new Or(EMPTY, r1, r2); @@ -769,8 +766,9 @@ public class OptimizerTests extends ESTestCase { public void testCombineBinaryComparisonsDisjunctionOfIncludedRange() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - Range r1 = new Range(EMPTY, fa, L(2), false, L(3), false); - Range r2 = new Range(EMPTY, fa, L(1), false, L(4), false); + + Range r1 = new Range(EMPTY, fa, TWO, false, THREE, false); + Range r2 = new Range(EMPTY, fa, ONE, false, FOUR, false); Or or = new Or(EMPTY, r1, r2); @@ -779,9 +777,9 @@ public class OptimizerTests extends ESTestCase { assertEquals(Range.class, exp.getClass()); Range r = (Range) exp; - assertEquals(L(1), r.lower()); + assertEquals(ONE, r.lower()); assertFalse(r.includeLower()); - assertEquals(L(4), r.upper()); + assertEquals(FOUR, r.upper()); assertFalse(r.includeUpper()); } @@ -789,8 +787,8 @@ public class OptimizerTests extends ESTestCase { public void testCombineBinaryComparisonsDisjunctionOfNonOverlappingBoundaries() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - Range r1 = new Range(EMPTY, fa, L(2), false, L(3), false); - Range r2 = new Range(EMPTY, fa, L(1), false, L(2), false); + Range r1 = new Range(EMPTY, fa, TWO, false, THREE, false); + Range r2 = new Range(EMPTY, fa, ONE, false, TWO, false); Or or = new Or(EMPTY, r1, r2); @@ -803,8 +801,8 @@ public class OptimizerTests extends ESTestCase { public void testCombineBinaryComparisonsDisjunctionOfUpperEqualsOverlappingBoundaries() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - Range r1 = new Range(EMPTY, fa, L(2), false, L(3), false); - Range r2 = new Range(EMPTY, fa, L(2), false, L(3), true); + Range r1 = new Range(EMPTY, fa, TWO, false, THREE, false); + Range r2 = new Range(EMPTY, fa, TWO, false, THREE, true); Or or = new Or(EMPTY, r1, r2); @@ -817,8 +815,8 @@ public class OptimizerTests extends ESTestCase { public void testCombineBinaryComparisonsOverlappingUpperBoundary() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - Range r2 = new Range(EMPTY, fa, L(2), false, L(3), false); - Range r1 = new Range(EMPTY, fa, L(1), false, L(3), false); + Range r2 = new Range(EMPTY, fa, TWO, false, THREE, false); + Range r1 = new Range(EMPTY, fa, ONE, false, THREE, false); Or or = new Or(EMPTY, r1, r2); @@ -831,8 +829,8 @@ public class OptimizerTests extends ESTestCase { public void testCombineBinaryComparisonsWithDifferentUpperLimitInclusion() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - Range r1 = new Range(EMPTY, fa, L(1), false, L(3), false); - Range r2 = new Range(EMPTY, fa, L(2), false, L(3), true); + Range r1 = new Range(EMPTY, fa, ONE, false, THREE, false); + Range r2 = new Range(EMPTY, fa, TWO, false, THREE, true); Or or = new Or(EMPTY, r1, r2); @@ -845,8 +843,8 @@ public class OptimizerTests extends ESTestCase { public void testRangesOverlappingNoLowerBoundary() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - Range r2 = new Range(EMPTY, fa, L(0), false, L(2), false); - Range r1 = new Range(EMPTY, fa, L(0), false, L(1), true); + Range r2 = new Range(EMPTY, fa, L(0), false, TWO, false); + Range r1 = new Range(EMPTY, fa, L(0), false, ONE, true); Or or = new Or(EMPTY, r1, r2); @@ -860,8 +858,8 @@ public class OptimizerTests extends ESTestCase { // a == 1 AND a == 2 -> FALSE public void testDualEqualsConjunction() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - Equals eq1 = new Equals(EMPTY, fa, L(1)); - Equals eq2 = new Equals(EMPTY, fa, L(2)); + Equals eq1 = new Equals(EMPTY, fa, ONE); + Equals eq2 = new Equals(EMPTY, fa, TWO); PropagateEquals rule = new PropagateEquals(); Expression exp = rule.rule(new And(EMPTY, eq1, eq2)); @@ -871,8 +869,8 @@ public class OptimizerTests extends ESTestCase { // 1 <= a < 10 AND a == 1 -> a == 1 public void testEliminateRangeByEqualsInInterval() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); - Equals eq1 = new Equals(EMPTY, fa, L(1)); - Range r = new Range(EMPTY, fa, L(1), true, L(10), false); + Equals eq1 = new Equals(EMPTY, fa, ONE); + Range r = new Range(EMPTY, fa, ONE, true, L(10), false); PropagateEquals rule = new PropagateEquals(); Expression exp = rule.rule(new And(EMPTY, eq1, r)); @@ -883,7 +881,7 @@ public class OptimizerTests extends ESTestCase { public void testEliminateRangeByEqualsOutsideInterval() { FieldAttribute fa = new FieldAttribute(EMPTY, "a", new EsField("af", DataType.INTEGER, emptyMap(), true)); Equals eq1 = new Equals(EMPTY, fa, L(10)); - Range r = new Range(EMPTY, fa, L(1), false, L(10), false); + Range r = new Range(EMPTY, fa, ONE, false, L(10), false); PropagateEquals rule = new PropagateEquals(); Expression exp = rule.rule(new And(EMPTY, eq1, r)); From 1e577d3ce8daa81530999426adddd9845332454d Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Tue, 11 Sep 2018 16:22:59 -0400 Subject: [PATCH 13/78] Mute testIndexDeletionWhenNodeRejoins Tracked at #33613 --- .../test/java/org/elasticsearch/gateway/GatewayIndexStateIT.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/test/java/org/elasticsearch/gateway/GatewayIndexStateIT.java b/server/src/test/java/org/elasticsearch/gateway/GatewayIndexStateIT.java index 4a0d6a8e888..3ac41ad04cf 100644 --- a/server/src/test/java/org/elasticsearch/gateway/GatewayIndexStateIT.java +++ b/server/src/test/java/org/elasticsearch/gateway/GatewayIndexStateIT.java @@ -322,6 +322,7 @@ public class GatewayIndexStateIT extends ESIntegTestCase { * This test ensures that when an index deletion takes place while a node is offline, when that * node rejoins the cluster, it deletes the index locally instead of importing it as a dangling index. */ + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/33613") public void testIndexDeletionWhenNodeRejoins() throws Exception { final String indexName = "test-index-del-on-node-rejoin-idx"; final int numNodes = 2; From eca37e6e0aa8b68096e86e8823626c43b9116328 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Tue, 11 Sep 2018 16:37:52 -0400 Subject: [PATCH 14/78] Expose CCR to the transport client (#33608) This commit exposes CCR to the transport client. --- .../java/org/elasticsearch/xpack/ccr/Ccr.java | 29 +- .../ccr/action/AutoFollowCoordinator.java | 2 + .../action/CreateAndFollowIndexAction.java | 367 ----------- .../xpack/ccr/action/FollowIndexAction.java | 571 ------------------ .../xpack/ccr/action/ShardChangesAction.java | 3 +- .../xpack/ccr/action/ShardFollowNodeTask.java | 504 +--------------- .../ccr/action/TransportCcrStatsAction.java | 1 + .../TransportCreateAndFollowIndexAction.java | 231 +++++++ .../action/TransportFollowIndexAction.java | 336 +++++++++++ .../action/TransportUnfollowIndexAction.java | 105 ++++ .../xpack/ccr/action/UnfollowIndexAction.java | 152 ----- .../xpack/ccr/rest/RestCcrStatsAction.java | 2 +- .../rest/RestCreateAndFollowIndexAction.java | 4 +- .../xpack/ccr/rest/RestFollowIndexAction.java | 4 +- .../ccr/rest/RestUnfollowIndexAction.java | 4 +- .../elasticsearch/xpack/ccr/CcrLicenseIT.java | 17 +- .../xpack/ccr/ShardChangesIT.java | 18 +- .../action/AutoFollowCoordinatorTests.java | 1 + .../CreateAndFollowIndexRequestTests.java | 1 + .../CreateAndFollowIndexResponseTests.java | 1 + .../ccr/action/FollowIndexRequestTests.java | 1 + .../ShardFollowNodeTaskRandomTests.java | 8 +- .../ShardFollowNodeTaskStatusTests.java | 17 +- .../ccr/action/ShardFollowNodeTaskTests.java | 49 +- ...a => TransportFollowIndexActionTests.java} | 34 +- .../elasticsearch/xpack/core/XPackClient.java | 10 +- .../core/ccr/ShardFollowNodeTaskStatus.java | 504 ++++++++++++++++ .../core}/ccr/action/CcrStatsAction.java | 15 +- .../action/CreateAndFollowIndexAction.java | 167 +++++ .../core/ccr/action/FollowIndexAction.java | 307 ++++++++++ .../core/ccr/action/UnfollowIndexAction.java | 62 ++ .../xpack/core/ccr/client/CcrClient.java | 73 +++ 32 files changed, 1911 insertions(+), 1689 deletions(-) delete mode 100644 x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/CreateAndFollowIndexAction.java delete mode 100644 x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/FollowIndexAction.java create mode 100644 x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportCreateAndFollowIndexAction.java create mode 100644 x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexAction.java create mode 100644 x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportUnfollowIndexAction.java delete mode 100644 x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/UnfollowIndexAction.java rename x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/{FollowIndexActionTests.java => TransportFollowIndexActionTests.java} (89%) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/ShardFollowNodeTaskStatus.java rename x-pack/plugin/{ccr/src/main/java/org/elasticsearch/xpack => core/src/main/java/org/elasticsearch/xpack/core}/ccr/action/CcrStatsAction.java (92%) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/CreateAndFollowIndexAction.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/FollowIndexAction.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/UnfollowIndexAction.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/client/CcrClient.java diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java index 353a66db263..66b8a5c9590 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java @@ -40,19 +40,21 @@ import org.elasticsearch.threadpool.FixedExecutorBuilder; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.ccr.action.AutoFollowCoordinator; -import org.elasticsearch.xpack.ccr.action.CcrStatsAction; -import org.elasticsearch.xpack.ccr.action.CreateAndFollowIndexAction; +import org.elasticsearch.xpack.ccr.action.TransportUnfollowIndexAction; +import org.elasticsearch.xpack.core.ccr.action.CcrStatsAction; +import org.elasticsearch.xpack.ccr.action.TransportCreateAndFollowIndexAction; +import org.elasticsearch.xpack.ccr.action.TransportFollowIndexAction; +import org.elasticsearch.xpack.core.ccr.action.CreateAndFollowIndexAction; import org.elasticsearch.xpack.ccr.action.DeleteAutoFollowPatternAction; -import org.elasticsearch.xpack.ccr.action.FollowIndexAction; +import org.elasticsearch.xpack.core.ccr.action.FollowIndexAction; import org.elasticsearch.xpack.ccr.action.PutAutoFollowPatternAction; import org.elasticsearch.xpack.ccr.action.ShardChangesAction; -import org.elasticsearch.xpack.ccr.action.ShardFollowNodeTask; import org.elasticsearch.xpack.ccr.action.ShardFollowTask; import org.elasticsearch.xpack.ccr.action.ShardFollowTasksExecutor; import org.elasticsearch.xpack.ccr.action.TransportCcrStatsAction; import org.elasticsearch.xpack.ccr.action.TransportDeleteAutoFollowPatternAction; import org.elasticsearch.xpack.ccr.action.TransportPutAutoFollowPatternAction; -import org.elasticsearch.xpack.ccr.action.UnfollowIndexAction; +import org.elasticsearch.xpack.core.ccr.action.UnfollowIndexAction; import org.elasticsearch.xpack.ccr.action.bulk.BulkShardOperationsAction; import org.elasticsearch.xpack.ccr.action.bulk.TransportBulkShardOperationsAction; import org.elasticsearch.xpack.ccr.index.engine.FollowingEngineFactory; @@ -63,6 +65,7 @@ import org.elasticsearch.xpack.ccr.rest.RestFollowIndexAction; import org.elasticsearch.xpack.ccr.rest.RestPutAutoFollowPatternAction; import org.elasticsearch.xpack.ccr.rest.RestUnfollowIndexAction; import org.elasticsearch.xpack.core.XPackPlugin; +import org.elasticsearch.xpack.core.ccr.ShardFollowNodeTaskStatus; import java.util.Arrays; import java.util.Collection; @@ -148,9 +151,9 @@ public class Ccr extends Plugin implements ActionPlugin, PersistentTaskPlugin, E // stats action new ActionHandler<>(CcrStatsAction.INSTANCE, TransportCcrStatsAction.class), // follow actions - new ActionHandler<>(CreateAndFollowIndexAction.INSTANCE, CreateAndFollowIndexAction.TransportAction.class), - new ActionHandler<>(FollowIndexAction.INSTANCE, FollowIndexAction.TransportAction.class), - new ActionHandler<>(UnfollowIndexAction.INSTANCE, UnfollowIndexAction.TransportAction.class), + new ActionHandler<>(CreateAndFollowIndexAction.INSTANCE, TransportCreateAndFollowIndexAction.class), + new ActionHandler<>(FollowIndexAction.INSTANCE, TransportFollowIndexAction.class), + new ActionHandler<>(UnfollowIndexAction.INSTANCE, TransportUnfollowIndexAction.class), // auto-follow actions new ActionHandler<>(DeleteAutoFollowPatternAction.INSTANCE, TransportDeleteAutoFollowPatternAction.class), new ActionHandler<>(PutAutoFollowPatternAction.INSTANCE, TransportPutAutoFollowPatternAction.class)); @@ -179,8 +182,8 @@ public class Ccr extends Plugin implements ActionPlugin, PersistentTaskPlugin, E ShardFollowTask::new), // Task statuses - new NamedWriteableRegistry.Entry(Task.Status.class, ShardFollowNodeTask.Status.STATUS_PARSER_NAME, - ShardFollowNodeTask.Status::new) + new NamedWriteableRegistry.Entry(Task.Status.class, ShardFollowNodeTaskStatus.STATUS_PARSER_NAME, + ShardFollowNodeTaskStatus::new) ); } @@ -192,9 +195,9 @@ public class Ccr extends Plugin implements ActionPlugin, PersistentTaskPlugin, E // Task statuses new NamedXContentRegistry.Entry( - ShardFollowNodeTask.Status.class, - new ParseField(ShardFollowNodeTask.Status.STATUS_PARSER_NAME), - ShardFollowNodeTask.Status::fromXContent)); + ShardFollowNodeTaskStatus.class, + new ParseField(ShardFollowNodeTaskStatus.STATUS_PARSER_NAME), + ShardFollowNodeTaskStatus::fromXContent)); } /** diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinator.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinator.java index e28214341a9..bc62e439538 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinator.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinator.java @@ -27,6 +27,8 @@ import org.elasticsearch.xpack.ccr.CcrLicenseChecker; import org.elasticsearch.xpack.ccr.CcrSettings; import org.elasticsearch.xpack.core.ccr.AutoFollowMetadata; import org.elasticsearch.xpack.core.ccr.AutoFollowMetadata.AutoFollowPattern; +import org.elasticsearch.xpack.core.ccr.action.CreateAndFollowIndexAction; +import org.elasticsearch.xpack.core.ccr.action.FollowIndexAction; import java.util.ArrayList; import java.util.HashMap; diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/CreateAndFollowIndexAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/CreateAndFollowIndexAction.java deleted file mode 100644 index 223f6ed8e6d..00000000000 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/CreateAndFollowIndexAction.java +++ /dev/null @@ -1,367 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -package org.elasticsearch.xpack.ccr.action; - -import com.carrotsearch.hppc.cursors.ObjectObjectCursor; -import org.elasticsearch.ResourceAlreadyExistsException; -import org.elasticsearch.action.Action; -import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.ActionRequestValidationException; -import org.elasticsearch.action.ActionResponse; -import org.elasticsearch.action.IndicesRequest; -import org.elasticsearch.action.support.ActionFilters; -import org.elasticsearch.action.support.ActiveShardCount; -import org.elasticsearch.action.support.ActiveShardsObserver; -import org.elasticsearch.action.support.IndicesOptions; -import org.elasticsearch.action.support.master.AcknowledgedRequest; -import org.elasticsearch.action.support.master.TransportMasterNodeAction; -import org.elasticsearch.client.Client; -import org.elasticsearch.cluster.AckedClusterStateUpdateTask; -import org.elasticsearch.cluster.ClusterState; -import org.elasticsearch.cluster.block.ClusterBlockException; -import org.elasticsearch.cluster.block.ClusterBlockLevel; -import org.elasticsearch.cluster.metadata.IndexMetaData; -import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; -import org.elasticsearch.cluster.metadata.MappingMetaData; -import org.elasticsearch.cluster.metadata.MetaData; -import org.elasticsearch.cluster.routing.RoutingTable; -import org.elasticsearch.cluster.routing.allocation.AllocationService; -import org.elasticsearch.cluster.service.ClusterService; -import org.elasticsearch.common.UUIDs; -import org.elasticsearch.common.inject.Inject; -import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.xcontent.ToXContentObject; -import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.license.LicenseUtils; -import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.transport.RemoteClusterAware; -import org.elasticsearch.transport.RemoteClusterService; -import org.elasticsearch.transport.TransportService; -import org.elasticsearch.xpack.ccr.CcrLicenseChecker; -import org.elasticsearch.xpack.ccr.CcrSettings; - -import java.io.IOException; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -public class CreateAndFollowIndexAction extends Action { - - public static final CreateAndFollowIndexAction INSTANCE = new CreateAndFollowIndexAction(); - public static final String NAME = "indices:admin/xpack/ccr/create_and_follow_index"; - - private CreateAndFollowIndexAction() { - super(NAME); - } - - @Override - public Response newResponse() { - return new Response(); - } - - public static class Request extends AcknowledgedRequest implements IndicesRequest { - - private FollowIndexAction.Request followRequest; - - public Request(FollowIndexAction.Request followRequest) { - this.followRequest = Objects.requireNonNull(followRequest); - } - - Request() { - } - - public FollowIndexAction.Request getFollowRequest() { - return followRequest; - } - - @Override - public ActionRequestValidationException validate() { - return followRequest.validate(); - } - - @Override - public String[] indices() { - return new String[]{followRequest.getFollowerIndex()}; - } - - @Override - public IndicesOptions indicesOptions() { - return IndicesOptions.strictSingleIndexNoExpandForbidClosed(); - } - - @Override - public void readFrom(StreamInput in) throws IOException { - super.readFrom(in); - followRequest = new FollowIndexAction.Request(); - followRequest.readFrom(in); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - super.writeTo(out); - followRequest.writeTo(out); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Request request = (Request) o; - return Objects.equals(followRequest, request.followRequest); - } - - @Override - public int hashCode() { - return Objects.hash(followRequest); - } - } - - public static class Response extends ActionResponse implements ToXContentObject { - - private boolean followIndexCreated; - private boolean followIndexShardsAcked; - private boolean indexFollowingStarted; - - Response() { - } - - Response(boolean followIndexCreated, boolean followIndexShardsAcked, boolean indexFollowingStarted) { - this.followIndexCreated = followIndexCreated; - this.followIndexShardsAcked = followIndexShardsAcked; - this.indexFollowingStarted = indexFollowingStarted; - } - - public boolean isFollowIndexCreated() { - return followIndexCreated; - } - - public boolean isFollowIndexShardsAcked() { - return followIndexShardsAcked; - } - - public boolean isIndexFollowingStarted() { - return indexFollowingStarted; - } - - @Override - public void readFrom(StreamInput in) throws IOException { - super.readFrom(in); - followIndexCreated = in.readBoolean(); - followIndexShardsAcked = in.readBoolean(); - indexFollowingStarted = in.readBoolean(); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - super.writeTo(out); - out.writeBoolean(followIndexCreated); - out.writeBoolean(followIndexShardsAcked); - out.writeBoolean(indexFollowingStarted); - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - { - builder.field("follow_index_created", followIndexCreated); - builder.field("follow_index_shards_acked", followIndexShardsAcked); - builder.field("index_following_started", indexFollowingStarted); - } - builder.endObject(); - return builder; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Response response = (Response) o; - return followIndexCreated == response.followIndexCreated && - followIndexShardsAcked == response.followIndexShardsAcked && - indexFollowingStarted == response.indexFollowingStarted; - } - - @Override - public int hashCode() { - return Objects.hash(followIndexCreated, followIndexShardsAcked, indexFollowingStarted); - } - } - - public static class TransportAction extends TransportMasterNodeAction { - - private final Client client; - private final AllocationService allocationService; - private final RemoteClusterService remoteClusterService; - private final ActiveShardsObserver activeShardsObserver; - private final CcrLicenseChecker ccrLicenseChecker; - - @Inject - public TransportAction( - final Settings settings, - final ThreadPool threadPool, - final TransportService transportService, - final ClusterService clusterService, - final ActionFilters actionFilters, - final IndexNameExpressionResolver indexNameExpressionResolver, - final Client client, - final AllocationService allocationService, - final CcrLicenseChecker ccrLicenseChecker) { - super(settings, NAME, transportService, clusterService, threadPool, actionFilters, indexNameExpressionResolver, Request::new); - this.client = client; - this.allocationService = allocationService; - this.remoteClusterService = transportService.getRemoteClusterService(); - this.activeShardsObserver = new ActiveShardsObserver(settings, clusterService, threadPool); - this.ccrLicenseChecker = Objects.requireNonNull(ccrLicenseChecker); - } - - @Override - protected String executor() { - return ThreadPool.Names.SAME; - } - - @Override - protected Response newResponse() { - return new Response(); - } - - @Override - protected void masterOperation( - final Request request, final ClusterState state, final ActionListener listener) throws Exception { - if (ccrLicenseChecker.isCcrAllowed() == false) { - listener.onFailure(LicenseUtils.newComplianceException("ccr")); - return; - } - final String[] indices = new String[]{request.getFollowRequest().getLeaderIndex()}; - final Map> remoteClusterIndices = remoteClusterService.groupClusterIndices(indices, s -> false); - if (remoteClusterIndices.containsKey(RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY)) { - createFollowerIndexAndFollowLocalIndex(request, state, listener); - } else { - assert remoteClusterIndices.size() == 1; - final Map.Entry> entry = remoteClusterIndices.entrySet().iterator().next(); - assert entry.getValue().size() == 1; - final String clusterAlias = entry.getKey(); - final String leaderIndex = entry.getValue().get(0); - createFollowerIndexAndFollowRemoteIndex(request, clusterAlias, leaderIndex, listener); - } - } - - private void createFollowerIndexAndFollowLocalIndex( - final Request request, final ClusterState state, final ActionListener listener) { - // following an index in local cluster, so use local cluster state to fetch leader index metadata - final IndexMetaData leaderIndexMetadata = state.getMetaData().index(request.getFollowRequest().getLeaderIndex()); - createFollowerIndex(leaderIndexMetadata, request, listener); - } - - private void createFollowerIndexAndFollowRemoteIndex( - final Request request, - final String clusterAlias, - final String leaderIndex, - final ActionListener listener) { - ccrLicenseChecker.checkRemoteClusterLicenseAndFetchLeaderIndexMetadata( - client, - clusterAlias, - leaderIndex, - listener::onFailure, - leaderIndexMetaData -> createFollowerIndex(leaderIndexMetaData, request, listener)); - } - - private void createFollowerIndex( - final IndexMetaData leaderIndexMetaData, final Request request, final ActionListener listener) { - if (leaderIndexMetaData == null) { - listener.onFailure(new IllegalArgumentException("leader index [" + request.getFollowRequest().getLeaderIndex() + - "] does not exist")); - return; - } - - ActionListener handler = ActionListener.wrap( - result -> { - if (result) { - initiateFollowing(request, listener); - } else { - listener.onResponse(new Response(true, false, false)); - } - }, - listener::onFailure); - // Can't use create index api here, because then index templates can alter the mappings / settings. - // And index templates could introduce settings / mappings that are incompatible with the leader index. - clusterService.submitStateUpdateTask("follow_index_action", new AckedClusterStateUpdateTask(request, handler) { - - @Override - protected Boolean newResponse(boolean acknowledged) { - return acknowledged; - } - - @Override - public ClusterState execute(ClusterState currentState) throws Exception { - String followIndex = request.getFollowRequest().getFollowerIndex(); - IndexMetaData currentIndex = currentState.metaData().index(followIndex); - if (currentIndex != null) { - throw new ResourceAlreadyExistsException(currentIndex.getIndex()); - } - - MetaData.Builder mdBuilder = MetaData.builder(currentState.metaData()); - IndexMetaData.Builder imdBuilder = IndexMetaData.builder(followIndex); - - // Copy all settings, but overwrite a few settings. - Settings.Builder settingsBuilder = Settings.builder(); - settingsBuilder.put(leaderIndexMetaData.getSettings()); - // Overwriting UUID here, because otherwise we can't follow indices in the same cluster - settingsBuilder.put(IndexMetaData.SETTING_INDEX_UUID, UUIDs.randomBase64UUID()); - settingsBuilder.put(IndexMetaData.SETTING_INDEX_PROVIDED_NAME, followIndex); - settingsBuilder.put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true); - imdBuilder.settings(settingsBuilder); - - // Copy mappings from leader IMD to follow IMD - for (ObjectObjectCursor cursor : leaderIndexMetaData.getMappings()) { - imdBuilder.putMapping(cursor.value); - } - imdBuilder.setRoutingNumShards(leaderIndexMetaData.getRoutingNumShards()); - IndexMetaData followIMD = imdBuilder.build(); - mdBuilder.put(followIMD, false); - - ClusterState.Builder builder = ClusterState.builder(currentState); - builder.metaData(mdBuilder.build()); - ClusterState updatedState = builder.build(); - - RoutingTable.Builder routingTableBuilder = RoutingTable.builder(updatedState.routingTable()) - .addAsNew(updatedState.metaData().index(request.getFollowRequest().getFollowerIndex())); - updatedState = allocationService.reroute( - ClusterState.builder(updatedState).routingTable(routingTableBuilder.build()).build(), - "follow index [" + request.getFollowRequest().getFollowerIndex() + "] created"); - - logger.info("[{}] creating index, cause [ccr_create_and_follow], shards [{}]/[{}]", - followIndex, followIMD.getNumberOfShards(), followIMD.getNumberOfReplicas()); - - return updatedState; - } - }); - } - - private void initiateFollowing(Request request, ActionListener listener) { - activeShardsObserver.waitForActiveShards(new String[]{request.followRequest.getFollowerIndex()}, - ActiveShardCount.DEFAULT, request.timeout(), result -> { - if (result) { - client.execute(FollowIndexAction.INSTANCE, request.getFollowRequest(), ActionListener.wrap( - r -> listener.onResponse(new Response(true, true, r.isAcknowledged())), - listener::onFailure - )); - } else { - listener.onResponse(new Response(true, false, false)); - } - }, listener::onFailure); - } - - @Override - protected ClusterBlockException checkBlock(Request request, ClusterState state) { - return state.blocks().indexBlockedException(ClusterBlockLevel.METADATA_WRITE, request.getFollowRequest().getFollowerIndex()); - } - - } - -} diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/FollowIndexAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/FollowIndexAction.java deleted file mode 100644 index 49822455110..00000000000 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/FollowIndexAction.java +++ /dev/null @@ -1,571 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -package org.elasticsearch.xpack.ccr.action; - -import org.elasticsearch.action.Action; -import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.ActionRequest; -import org.elasticsearch.action.ActionRequestValidationException; -import org.elasticsearch.action.support.ActionFilters; -import org.elasticsearch.action.support.HandledTransportAction; -import org.elasticsearch.action.support.master.AcknowledgedResponse; -import org.elasticsearch.client.Client; -import org.elasticsearch.cluster.ClusterState; -import org.elasticsearch.cluster.metadata.IndexMetaData; -import org.elasticsearch.cluster.routing.allocation.decider.EnableAllocationDecider; -import org.elasticsearch.cluster.routing.allocation.decider.ShardsLimitAllocationDecider; -import org.elasticsearch.cluster.service.ClusterService; -import org.elasticsearch.common.ParseField; -import org.elasticsearch.common.inject.Inject; -import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.common.settings.Setting; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.common.xcontent.ConstructingObjectParser; -import org.elasticsearch.common.xcontent.ObjectParser; -import org.elasticsearch.common.xcontent.ToXContentObject; -import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.index.IndexSettings; -import org.elasticsearch.index.IndexingSlowLog; -import org.elasticsearch.index.SearchSlowLog; -import org.elasticsearch.index.cache.bitset.BitsetFilterCache; -import org.elasticsearch.index.mapper.MapperService; -import org.elasticsearch.index.shard.ShardId; -import org.elasticsearch.indices.IndicesRequestCache; -import org.elasticsearch.indices.IndicesService; -import org.elasticsearch.license.LicenseUtils; -import org.elasticsearch.persistent.PersistentTasksCustomMetaData; -import org.elasticsearch.persistent.PersistentTasksService; -import org.elasticsearch.tasks.Task; -import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.transport.RemoteClusterAware; -import org.elasticsearch.transport.RemoteClusterService; -import org.elasticsearch.transport.TransportService; -import org.elasticsearch.xpack.ccr.CcrLicenseChecker; -import org.elasticsearch.xpack.ccr.CcrSettings; - -import java.io.IOException; -import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReferenceArray; -import java.util.stream.Collectors; - -public class FollowIndexAction extends Action { - - public static final FollowIndexAction INSTANCE = new FollowIndexAction(); - public static final String NAME = "cluster:admin/xpack/ccr/follow_index"; - - private FollowIndexAction() { - super(NAME); - } - - @Override - public AcknowledgedResponse newResponse() { - return new AcknowledgedResponse(); - } - - public static class Request extends ActionRequest implements ToXContentObject { - - private static final ParseField LEADER_INDEX_FIELD = new ParseField("leader_index"); - private static final ParseField FOLLOWER_INDEX_FIELD = new ParseField("follower_index"); - private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, true, - (args, followerIndex) -> { - if (args[1] != null) { - followerIndex = (String) args[1]; - } - return new Request((String) args[0], followerIndex, (Integer) args[2], (Integer) args[3], (Long) args[4], - (Integer) args[5], (Integer) args[6], (TimeValue) args[7], (TimeValue) args[8]); - }); - - static { - PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), LEADER_INDEX_FIELD); - PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), FOLLOWER_INDEX_FIELD); - PARSER.declareInt(ConstructingObjectParser.optionalConstructorArg(), ShardFollowTask.MAX_BATCH_OPERATION_COUNT); - PARSER.declareInt(ConstructingObjectParser.optionalConstructorArg(), ShardFollowTask.MAX_CONCURRENT_READ_BATCHES); - PARSER.declareLong(ConstructingObjectParser.optionalConstructorArg(), ShardFollowTask.MAX_BATCH_SIZE_IN_BYTES); - PARSER.declareInt(ConstructingObjectParser.optionalConstructorArg(), ShardFollowTask.MAX_CONCURRENT_WRITE_BATCHES); - PARSER.declareInt(ConstructingObjectParser.optionalConstructorArg(), ShardFollowTask.MAX_WRITE_BUFFER_SIZE); - PARSER.declareField(ConstructingObjectParser.optionalConstructorArg(), - (p, c) -> TimeValue.parseTimeValue(p.text(), ShardFollowTask.RETRY_TIMEOUT.getPreferredName()), - ShardFollowTask.RETRY_TIMEOUT, ObjectParser.ValueType.STRING); - PARSER.declareField(ConstructingObjectParser.optionalConstructorArg(), - (p, c) -> TimeValue.parseTimeValue(p.text(), ShardFollowTask.IDLE_SHARD_RETRY_DELAY.getPreferredName()), - ShardFollowTask.IDLE_SHARD_RETRY_DELAY, ObjectParser.ValueType.STRING); - } - - public static Request fromXContent(XContentParser parser, String followerIndex) throws IOException { - Request request = PARSER.parse(parser, followerIndex); - if (followerIndex != null) { - if (request.followerIndex == null) { - request.followerIndex = followerIndex; - } else { - if (request.followerIndex.equals(followerIndex) == false) { - throw new IllegalArgumentException("provided follower_index is not equal"); - } - } - } - return request; - } - - private String leaderIndex; - private String followerIndex; - private int maxBatchOperationCount; - private int maxConcurrentReadBatches; - private long maxOperationSizeInBytes; - private int maxConcurrentWriteBatches; - private int maxWriteBufferSize; - private TimeValue retryTimeout; - private TimeValue idleShardRetryDelay; - - public Request( - String leaderIndex, - String followerIndex, - Integer maxBatchOperationCount, - Integer maxConcurrentReadBatches, - Long maxOperationSizeInBytes, - Integer maxConcurrentWriteBatches, - Integer maxWriteBufferSize, - TimeValue retryTimeout, - TimeValue idleShardRetryDelay) { - - if (leaderIndex == null) { - throw new IllegalArgumentException("leader_index is missing"); - } - if (followerIndex == null) { - throw new IllegalArgumentException("follower_index is missing"); - } - if (maxBatchOperationCount == null) { - maxBatchOperationCount = ShardFollowNodeTask.DEFAULT_MAX_BATCH_OPERATION_COUNT; - } - if (maxConcurrentReadBatches == null) { - maxConcurrentReadBatches = ShardFollowNodeTask.DEFAULT_MAX_CONCURRENT_READ_BATCHES; - } - if (maxOperationSizeInBytes == null) { - maxOperationSizeInBytes = ShardFollowNodeTask.DEFAULT_MAX_BATCH_SIZE_IN_BYTES; - } - if (maxConcurrentWriteBatches == null) { - maxConcurrentWriteBatches = ShardFollowNodeTask.DEFAULT_MAX_CONCURRENT_WRITE_BATCHES; - } - if (maxWriteBufferSize == null) { - maxWriteBufferSize = ShardFollowNodeTask.DEFAULT_MAX_WRITE_BUFFER_SIZE; - } - if (retryTimeout == null) { - retryTimeout = ShardFollowNodeTask.DEFAULT_RETRY_TIMEOUT; - } - if (idleShardRetryDelay == null) { - idleShardRetryDelay = ShardFollowNodeTask.DEFAULT_IDLE_SHARD_RETRY_DELAY; - } - - if (maxBatchOperationCount < 1) { - throw new IllegalArgumentException("maxBatchOperationCount must be larger than 0"); - } - if (maxConcurrentReadBatches < 1) { - throw new IllegalArgumentException("concurrent_processors must be larger than 0"); - } - if (maxOperationSizeInBytes <= 0) { - throw new IllegalArgumentException("processor_max_translog_bytes must be larger than 0"); - } - if (maxConcurrentWriteBatches < 1) { - throw new IllegalArgumentException("maxConcurrentWriteBatches must be larger than 0"); - } - if (maxWriteBufferSize < 1) { - throw new IllegalArgumentException("maxWriteBufferSize must be larger than 0"); - } - - this.leaderIndex = leaderIndex; - this.followerIndex = followerIndex; - this.maxBatchOperationCount = maxBatchOperationCount; - this.maxConcurrentReadBatches = maxConcurrentReadBatches; - this.maxOperationSizeInBytes = maxOperationSizeInBytes; - this.maxConcurrentWriteBatches = maxConcurrentWriteBatches; - this.maxWriteBufferSize = maxWriteBufferSize; - this.retryTimeout = retryTimeout; - this.idleShardRetryDelay = idleShardRetryDelay; - } - - Request() { - } - - public String getLeaderIndex() { - return leaderIndex; - } - - public String getFollowerIndex() { - return followerIndex; - } - - public int getMaxBatchOperationCount() { - return maxBatchOperationCount; - } - - @Override - public ActionRequestValidationException validate() { - return null; - } - - @Override - public void readFrom(StreamInput in) throws IOException { - super.readFrom(in); - leaderIndex = in.readString(); - followerIndex = in.readString(); - maxBatchOperationCount = in.readVInt(); - maxConcurrentReadBatches = in.readVInt(); - maxOperationSizeInBytes = in.readVLong(); - maxConcurrentWriteBatches = in.readVInt(); - maxWriteBufferSize = in.readVInt(); - retryTimeout = in.readOptionalTimeValue(); - idleShardRetryDelay = in.readOptionalTimeValue(); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - super.writeTo(out); - out.writeString(leaderIndex); - out.writeString(followerIndex); - out.writeVInt(maxBatchOperationCount); - out.writeVInt(maxConcurrentReadBatches); - out.writeVLong(maxOperationSizeInBytes); - out.writeVInt(maxConcurrentWriteBatches); - out.writeVInt(maxWriteBufferSize); - out.writeOptionalTimeValue(retryTimeout); - out.writeOptionalTimeValue(idleShardRetryDelay); - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - { - builder.field(LEADER_INDEX_FIELD.getPreferredName(), leaderIndex); - builder.field(FOLLOWER_INDEX_FIELD.getPreferredName(), followerIndex); - builder.field(ShardFollowTask.MAX_BATCH_OPERATION_COUNT.getPreferredName(), maxBatchOperationCount); - builder.field(ShardFollowTask.MAX_BATCH_SIZE_IN_BYTES.getPreferredName(), maxOperationSizeInBytes); - builder.field(ShardFollowTask.MAX_WRITE_BUFFER_SIZE.getPreferredName(), maxWriteBufferSize); - builder.field(ShardFollowTask.MAX_CONCURRENT_READ_BATCHES.getPreferredName(), maxConcurrentReadBatches); - builder.field(ShardFollowTask.MAX_CONCURRENT_WRITE_BATCHES.getPreferredName(), maxConcurrentWriteBatches); - builder.field(ShardFollowTask.RETRY_TIMEOUT.getPreferredName(), retryTimeout.getStringRep()); - builder.field(ShardFollowTask.IDLE_SHARD_RETRY_DELAY.getPreferredName(), idleShardRetryDelay.getStringRep()); - } - builder.endObject(); - return builder; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Request request = (Request) o; - return maxBatchOperationCount == request.maxBatchOperationCount && - maxConcurrentReadBatches == request.maxConcurrentReadBatches && - maxOperationSizeInBytes == request.maxOperationSizeInBytes && - maxConcurrentWriteBatches == request.maxConcurrentWriteBatches && - maxWriteBufferSize == request.maxWriteBufferSize && - Objects.equals(retryTimeout, request.retryTimeout) && - Objects.equals(idleShardRetryDelay, request.idleShardRetryDelay) && - Objects.equals(leaderIndex, request.leaderIndex) && - Objects.equals(followerIndex, request.followerIndex); - } - - @Override - public int hashCode() { - return Objects.hash( - leaderIndex, - followerIndex, - maxBatchOperationCount, - maxConcurrentReadBatches, - maxOperationSizeInBytes, - maxConcurrentWriteBatches, - maxWriteBufferSize, - retryTimeout, - idleShardRetryDelay - ); - } - } - - public static class TransportAction extends HandledTransportAction { - - private final Client client; - private final ThreadPool threadPool; - private final ClusterService clusterService; - private final RemoteClusterService remoteClusterService; - private final PersistentTasksService persistentTasksService; - private final IndicesService indicesService; - private final CcrLicenseChecker ccrLicenseChecker; - - @Inject - public TransportAction( - final Settings settings, - final ThreadPool threadPool, - final TransportService transportService, - final ActionFilters actionFilters, - final Client client, - final ClusterService clusterService, - final PersistentTasksService persistentTasksService, - final IndicesService indicesService, - final CcrLicenseChecker ccrLicenseChecker) { - super(settings, NAME, transportService, actionFilters, Request::new); - this.client = client; - this.threadPool = threadPool; - this.clusterService = clusterService; - this.remoteClusterService = transportService.getRemoteClusterService(); - this.persistentTasksService = persistentTasksService; - this.indicesService = indicesService; - this.ccrLicenseChecker = Objects.requireNonNull(ccrLicenseChecker); - } - - @Override - protected void doExecute(final Task task, - final Request request, - final ActionListener listener) { - if (ccrLicenseChecker.isCcrAllowed() == false) { - listener.onFailure(LicenseUtils.newComplianceException("ccr")); - return; - } - final String[] indices = new String[]{request.leaderIndex}; - final Map> remoteClusterIndices = remoteClusterService.groupClusterIndices(indices, s -> false); - if (remoteClusterIndices.containsKey(RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY)) { - followLocalIndex(request, listener); - } else { - assert remoteClusterIndices.size() == 1; - final Map.Entry> entry = remoteClusterIndices.entrySet().iterator().next(); - assert entry.getValue().size() == 1; - final String clusterAlias = entry.getKey(); - final String leaderIndex = entry.getValue().get(0); - followRemoteIndex(request, clusterAlias, leaderIndex, listener); - } - } - - private void followLocalIndex(final Request request, - final ActionListener listener) { - final ClusterState state = clusterService.state(); - final IndexMetaData followerIndexMetadata = state.getMetaData().index(request.getFollowerIndex()); - // following an index in local cluster, so use local cluster state to fetch leader index metadata - final IndexMetaData leaderIndexMetadata = state.getMetaData().index(request.getLeaderIndex()); - try { - start(request, null, leaderIndexMetadata, followerIndexMetadata, listener); - } catch (final IOException e) { - listener.onFailure(e); - } - } - - private void followRemoteIndex( - final Request request, - final String clusterAlias, - final String leaderIndex, - final ActionListener listener) { - final ClusterState state = clusterService.state(); - final IndexMetaData followerIndexMetadata = state.getMetaData().index(request.getFollowerIndex()); - ccrLicenseChecker.checkRemoteClusterLicenseAndFetchLeaderIndexMetadata( - client, - clusterAlias, - leaderIndex, - listener::onFailure, - leaderIndexMetadata -> { - try { - start(request, clusterAlias, leaderIndexMetadata, followerIndexMetadata, listener); - } catch (final IOException e) { - listener.onFailure(e); - } - }); - } - - /** - * Performs validation on the provided leader and follow {@link IndexMetaData} instances and then - * creates a persistent task for each leader primary shard. This persistent tasks track changes in the leader - * shard and replicate these changes to a follower shard. - * - * Currently the following validation is performed: - *
    - *
  • The leader index and follow index need to have the same number of primary shards
  • - *
- */ - void start( - Request request, - String clusterNameAlias, - IndexMetaData leaderIndexMetadata, - IndexMetaData followIndexMetadata, - ActionListener handler) throws IOException { - - MapperService mapperService = followIndexMetadata != null ? indicesService.createIndexMapperService(followIndexMetadata) : null; - validate(request, leaderIndexMetadata, followIndexMetadata, mapperService); - final int numShards = followIndexMetadata.getNumberOfShards(); - final AtomicInteger counter = new AtomicInteger(numShards); - final AtomicReferenceArray responses = new AtomicReferenceArray<>(followIndexMetadata.getNumberOfShards()); - Map filteredHeaders = threadPool.getThreadContext().getHeaders().entrySet().stream() - .filter(e -> ShardFollowTask.HEADER_FILTERS.contains(e.getKey())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));for (int i = 0; i < numShards; i++) { - final int shardId = i; - String taskId = followIndexMetadata.getIndexUUID() + "-" + shardId; - - ShardFollowTask shardFollowTask = new ShardFollowTask(clusterNameAlias, - new ShardId(followIndexMetadata.getIndex(), shardId), - new ShardId(leaderIndexMetadata.getIndex(), shardId), - request.maxBatchOperationCount, request.maxConcurrentReadBatches, request.maxOperationSizeInBytes, - request.maxConcurrentWriteBatches, request.maxWriteBufferSize, request.retryTimeout, - request.idleShardRetryDelay, filteredHeaders); - persistentTasksService.sendStartRequest(taskId, ShardFollowTask.NAME, shardFollowTask, - new ActionListener>() { - @Override - public void onResponse(PersistentTasksCustomMetaData.PersistentTask task) { - responses.set(shardId, task); - finalizeResponse(); - } - - @Override - public void onFailure(Exception e) { - responses.set(shardId, e); - finalizeResponse(); - } - - void finalizeResponse() { - Exception error = null; - if (counter.decrementAndGet() == 0) { - for (int j = 0; j < responses.length(); j++) { - Object response = responses.get(j); - if (response instanceof Exception) { - if (error == null) { - error = (Exception) response; - } else { - error.addSuppressed((Throwable) response); - } - } - } - - if (error == null) { - // include task ids? - handler.onResponse(new AcknowledgedResponse(true)); - } else { - // TODO: cancel all started tasks - handler.onFailure(error); - } - } - } - } - ); - } - } - } - - private static final Set> WHITELISTED_SETTINGS; - - static { - Set> whiteListedSettings = new HashSet<>(); - whiteListedSettings.add(IndexMetaData.INDEX_NUMBER_OF_REPLICAS_SETTING); - whiteListedSettings.add(IndexMetaData.INDEX_AUTO_EXPAND_REPLICAS_SETTING); - - whiteListedSettings.add(IndexMetaData.INDEX_ROUTING_EXCLUDE_GROUP_SETTING); - whiteListedSettings.add(IndexMetaData.INDEX_ROUTING_INCLUDE_GROUP_SETTING); - whiteListedSettings.add(IndexMetaData.INDEX_ROUTING_REQUIRE_GROUP_SETTING); - whiteListedSettings.add(EnableAllocationDecider.INDEX_ROUTING_REBALANCE_ENABLE_SETTING); - whiteListedSettings.add(EnableAllocationDecider.INDEX_ROUTING_ALLOCATION_ENABLE_SETTING); - whiteListedSettings.add(ShardsLimitAllocationDecider.INDEX_TOTAL_SHARDS_PER_NODE_SETTING); - - whiteListedSettings.add(IndicesRequestCache.INDEX_CACHE_REQUEST_ENABLED_SETTING); - whiteListedSettings.add(IndexSettings.MAX_RESULT_WINDOW_SETTING); - whiteListedSettings.add(IndexSettings.INDEX_WARMER_ENABLED_SETTING); - whiteListedSettings.add(IndexSettings.INDEX_REFRESH_INTERVAL_SETTING); - whiteListedSettings.add(IndexSettings.MAX_RESCORE_WINDOW_SETTING); - whiteListedSettings.add(IndexSettings.MAX_INNER_RESULT_WINDOW_SETTING); - whiteListedSettings.add(IndexSettings.DEFAULT_FIELD_SETTING); - whiteListedSettings.add(IndexSettings.QUERY_STRING_LENIENT_SETTING); - whiteListedSettings.add(IndexSettings.QUERY_STRING_ANALYZE_WILDCARD); - whiteListedSettings.add(IndexSettings.QUERY_STRING_ALLOW_LEADING_WILDCARD); - whiteListedSettings.add(IndexSettings.ALLOW_UNMAPPED); - whiteListedSettings.add(IndexSettings.INDEX_SEARCH_IDLE_AFTER); - whiteListedSettings.add(BitsetFilterCache.INDEX_LOAD_RANDOM_ACCESS_FILTERS_EAGERLY_SETTING); - - whiteListedSettings.add(SearchSlowLog.INDEX_SEARCH_SLOWLOG_THRESHOLD_FETCH_DEBUG_SETTING); - whiteListedSettings.add(SearchSlowLog.INDEX_SEARCH_SLOWLOG_THRESHOLD_FETCH_WARN_SETTING); - whiteListedSettings.add(SearchSlowLog.INDEX_SEARCH_SLOWLOG_THRESHOLD_FETCH_INFO_SETTING); - whiteListedSettings.add(SearchSlowLog.INDEX_SEARCH_SLOWLOG_THRESHOLD_FETCH_TRACE_SETTING); - whiteListedSettings.add(SearchSlowLog.INDEX_SEARCH_SLOWLOG_THRESHOLD_QUERY_WARN_SETTING); - whiteListedSettings.add(SearchSlowLog.INDEX_SEARCH_SLOWLOG_THRESHOLD_QUERY_DEBUG_SETTING); - whiteListedSettings.add(SearchSlowLog.INDEX_SEARCH_SLOWLOG_THRESHOLD_QUERY_INFO_SETTING); - whiteListedSettings.add(SearchSlowLog.INDEX_SEARCH_SLOWLOG_THRESHOLD_QUERY_TRACE_SETTING); - whiteListedSettings.add(SearchSlowLog.INDEX_SEARCH_SLOWLOG_LEVEL); - whiteListedSettings.add(IndexingSlowLog.INDEX_INDEXING_SLOWLOG_THRESHOLD_INDEX_WARN_SETTING); - whiteListedSettings.add(IndexingSlowLog.INDEX_INDEXING_SLOWLOG_THRESHOLD_INDEX_DEBUG_SETTING); - whiteListedSettings.add(IndexingSlowLog.INDEX_INDEXING_SLOWLOG_THRESHOLD_INDEX_INFO_SETTING); - whiteListedSettings.add(IndexingSlowLog.INDEX_INDEXING_SLOWLOG_THRESHOLD_INDEX_TRACE_SETTING); - whiteListedSettings.add(IndexingSlowLog.INDEX_INDEXING_SLOWLOG_LEVEL_SETTING); - whiteListedSettings.add(IndexingSlowLog.INDEX_INDEXING_SLOWLOG_REFORMAT_SETTING); - whiteListedSettings.add(IndexingSlowLog.INDEX_INDEXING_SLOWLOG_MAX_SOURCE_CHARS_TO_LOG_SETTING); - - whiteListedSettings.add(IndexSettings.INDEX_SOFT_DELETES_SETTING); - whiteListedSettings.add(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING); - - WHITELISTED_SETTINGS = Collections.unmodifiableSet(whiteListedSettings); - } - - static void validate(Request request, - IndexMetaData leaderIndex, - IndexMetaData followIndex, MapperService followerMapperService) { - if (leaderIndex == null) { - throw new IllegalArgumentException("leader index [" + request.leaderIndex + "] does not exist"); - } - if (followIndex == null) { - throw new IllegalArgumentException("follow index [" + request.followerIndex + "] does not exist"); - } - if (leaderIndex.getSettings().getAsBoolean(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), false) == false) { - throw new IllegalArgumentException("leader index [" + request.leaderIndex + "] does not have soft deletes enabled"); - } - if (leaderIndex.getNumberOfShards() != followIndex.getNumberOfShards()) { - throw new IllegalArgumentException("leader index primary shards [" + leaderIndex.getNumberOfShards() + - "] does not match with the number of shards of the follow index [" + followIndex.getNumberOfShards() + "]"); - } - if (leaderIndex.getRoutingNumShards() != followIndex.getRoutingNumShards()) { - throw new IllegalArgumentException("leader index number_of_routing_shards [" + leaderIndex.getRoutingNumShards() + - "] does not match with the number_of_routing_shards of the follow index [" + followIndex.getRoutingNumShards() + "]"); - } - if (leaderIndex.getState() != IndexMetaData.State.OPEN || followIndex.getState() != IndexMetaData.State.OPEN) { - throw new IllegalArgumentException("leader and follow index must be open"); - } - if (CcrSettings.CCR_FOLLOWING_INDEX_SETTING.get(followIndex.getSettings()) == false) { - throw new IllegalArgumentException("the following index [" + request.followerIndex + "] is not ready " + - "to follow; the setting [" + CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey() + "] must be enabled."); - } - // Make a copy, remove settings that are allowed to be different and then compare if the settings are equal. - Settings leaderSettings = filter(leaderIndex.getSettings()); - Settings followerSettings = filter(followIndex.getSettings()); - if (leaderSettings.equals(followerSettings) == false) { - throw new IllegalArgumentException("the leader and follower index settings must be identical"); - } - - // Validates if the current follower mapping is mergable with the leader mapping. - // This also validates for example whether specific mapper plugins have been installed - followerMapperService.merge(leaderIndex, MapperService.MergeReason.MAPPING_RECOVERY); - } - - private static Settings filter(Settings originalSettings) { - Settings.Builder settings = Settings.builder().put(originalSettings); - // Remove settings that are always going to be different between leader and follow index: - settings.remove(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey()); - settings.remove(IndexMetaData.SETTING_INDEX_UUID); - settings.remove(IndexMetaData.SETTING_INDEX_PROVIDED_NAME); - settings.remove(IndexMetaData.SETTING_CREATION_DATE); - - Iterator iterator = settings.keys().iterator(); - while (iterator.hasNext()) { - String key = iterator.next(); - for (Setting whitelistedSetting : WHITELISTED_SETTINGS) { - if (whitelistedSetting.match(key)) { - iterator.remove(); - break; - } - } - } - return settings.build(); - } - -} diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardChangesAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardChangesAction.java index d102c6b5b7a..b6f82783a56 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardChangesAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardChangesAction.java @@ -29,6 +29,7 @@ import org.elasticsearch.index.translog.Translog; import org.elasticsearch.indices.IndicesService; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.ccr.action.FollowIndexAction; import java.io.IOException; import java.util.ArrayList; @@ -57,7 +58,7 @@ public class ShardChangesAction extends Action { private long fromSeqNo; private int maxOperationCount; private ShardId shardId; - private long maxOperationSizeInBytes = ShardFollowNodeTask.DEFAULT_MAX_BATCH_SIZE_IN_BYTES; + private long maxOperationSizeInBytes = FollowIndexAction.DEFAULT_MAX_BATCH_SIZE_IN_BYTES; public Request(ShardId shardId) { super(shardId.getIndexName()); diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTask.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTask.java index 00e3aaaae2a..0a0a6877dc9 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTask.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTask.java @@ -10,35 +10,23 @@ import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.support.TransportActions; -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.logging.Loggers; import org.elasticsearch.common.transport.NetworkExceptionHelper; -import org.elasticsearch.common.unit.ByteSizeUnit; -import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.common.xcontent.ConstructingObjectParser; -import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.translog.Translog; import org.elasticsearch.persistent.AllocatedPersistentTask; -import org.elasticsearch.tasks.Task; import org.elasticsearch.tasks.TaskId; import org.elasticsearch.xpack.ccr.action.bulk.BulkShardOperationsResponse; +import org.elasticsearch.xpack.core.ccr.action.FollowIndexAction; +import org.elasticsearch.xpack.core.ccr.ShardFollowNodeTaskStatus; -import java.io.IOException; -import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.NavigableMap; -import java.util.Objects; import java.util.PriorityQueue; import java.util.Queue; import java.util.TreeMap; @@ -48,7 +36,6 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.LongConsumer; import java.util.function.LongSupplier; -import java.util.stream.Collectors; /** * The node task that fetch the write operations from a leader shard and @@ -56,15 +43,6 @@ import java.util.stream.Collectors; */ public abstract class ShardFollowNodeTask extends AllocatedPersistentTask { - public static final int DEFAULT_MAX_BATCH_OPERATION_COUNT = 1024; - public static final int DEFAULT_MAX_CONCURRENT_READ_BATCHES = 1; - public static final int DEFAULT_MAX_CONCURRENT_WRITE_BATCHES = 1; - public static final int DEFAULT_MAX_WRITE_BUFFER_SIZE = 10240; - public static final long DEFAULT_MAX_BATCH_SIZE_IN_BYTES = Long.MAX_VALUE; - private static final int RETRY_LIMIT = 10; - public static final TimeValue DEFAULT_RETRY_TIMEOUT = new TimeValue(500); - public static final TimeValue DEFAULT_IDLE_SHARD_RETRY_DELAY = TimeValue.timeValueSeconds(10); - private static final Logger LOGGER = Loggers.getLogger(ShardFollowNodeTask.class); private final String leaderIndex; @@ -380,7 +358,7 @@ public abstract class ShardFollowNodeTask extends AllocatedPersistentTask { private void handleFailure(Exception e, AtomicInteger retryCounter, Runnable task) { assert e != null; if (shouldRetry(e)) { - if (isStopped() == false && retryCounter.incrementAndGet() <= RETRY_LIMIT) { + if (isStopped() == false && retryCounter.incrementAndGet() <= FollowIndexAction.RETRY_LIMIT) { LOGGER.debug(new ParameterizedMessage("{} error during follow shard task, retrying...", params.getFollowShardId()), e); scheduler.accept(retryTimeout, task); } else { @@ -421,7 +399,7 @@ public abstract class ShardFollowNodeTask extends AllocatedPersistentTask { } @Override - public synchronized Status getStatus() { + public synchronized ShardFollowNodeTaskStatus getStatus() { final long timeSinceLastFetchMillis; if (lastFetchTime != -1) { timeSinceLastFetchMillis = TimeUnit.NANOSECONDS.toMillis(relativeTimeProvider.getAsLong() - lastFetchTime); @@ -429,7 +407,7 @@ public abstract class ShardFollowNodeTask extends AllocatedPersistentTask { // To avoid confusion when ccr didn't yet execute a fetch: timeSinceLastFetchMillis = -1; } - return new Status( + return new ShardFollowNodeTaskStatus( leaderIndex, getFollowShardId().getId(), leaderGlobalCheckpoint, @@ -454,476 +432,4 @@ public abstract class ShardFollowNodeTask extends AllocatedPersistentTask { timeSinceLastFetchMillis); } - public static class Status implements Task.Status { - - public static final String STATUS_PARSER_NAME = "shard-follow-node-task-status"; - - static final ParseField LEADER_INDEX = new ParseField("leader_index"); - static final ParseField SHARD_ID = new ParseField("shard_id"); - static final ParseField LEADER_GLOBAL_CHECKPOINT_FIELD = new ParseField("leader_global_checkpoint"); - static final ParseField LEADER_MAX_SEQ_NO_FIELD = new ParseField("leader_max_seq_no"); - static final ParseField FOLLOWER_GLOBAL_CHECKPOINT_FIELD = new ParseField("follower_global_checkpoint"); - static final ParseField FOLLOWER_MAX_SEQ_NO_FIELD = new ParseField("follower_max_seq_no"); - static final ParseField LAST_REQUESTED_SEQ_NO_FIELD = new ParseField("last_requested_seq_no"); - static final ParseField NUMBER_OF_CONCURRENT_READS_FIELD = new ParseField("number_of_concurrent_reads"); - static final ParseField NUMBER_OF_CONCURRENT_WRITES_FIELD = new ParseField("number_of_concurrent_writes"); - static final ParseField NUMBER_OF_QUEUED_WRITES_FIELD = new ParseField("number_of_queued_writes"); - static final ParseField MAPPING_VERSION_FIELD = new ParseField("mapping_version"); - static final ParseField TOTAL_FETCH_TIME_MILLIS_FIELD = new ParseField("total_fetch_time_millis"); - static final ParseField NUMBER_OF_SUCCESSFUL_FETCHES_FIELD = new ParseField("number_of_successful_fetches"); - static final ParseField NUMBER_OF_FAILED_FETCHES_FIELD = new ParseField("number_of_failed_fetches"); - static final ParseField OPERATIONS_RECEIVED_FIELD = new ParseField("operations_received"); - static final ParseField TOTAL_TRANSFERRED_BYTES = new ParseField("total_transferred_bytes"); - static final ParseField TOTAL_INDEX_TIME_MILLIS_FIELD = new ParseField("total_index_time_millis"); - static final ParseField NUMBER_OF_SUCCESSFUL_BULK_OPERATIONS_FIELD = new ParseField("number_of_successful_bulk_operations"); - static final ParseField NUMBER_OF_FAILED_BULK_OPERATIONS_FIELD = new ParseField("number_of_failed_bulk_operations"); - static final ParseField NUMBER_OF_OPERATIONS_INDEXED_FIELD = new ParseField("number_of_operations_indexed"); - static final ParseField FETCH_EXCEPTIONS = new ParseField("fetch_exceptions"); - static final ParseField TIME_SINCE_LAST_FETCH_MILLIS_FIELD = new ParseField("time_since_last_fetch_millis"); - - @SuppressWarnings("unchecked") - static final ConstructingObjectParser STATUS_PARSER = new ConstructingObjectParser<>(STATUS_PARSER_NAME, - args -> new Status( - (String) args[0], - (int) args[1], - (long) args[2], - (long) args[3], - (long) args[4], - (long) args[5], - (long) args[6], - (int) args[7], - (int) args[8], - (int) args[9], - (long) args[10], - (long) args[11], - (long) args[12], - (long) args[13], - (long) args[14], - (long) args[15], - (long) args[16], - (long) args[17], - (long) args[18], - (long) args[19], - new TreeMap<>( - ((List>) args[20]) - .stream() - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))), - (long) args[21])); - - public static final String FETCH_EXCEPTIONS_ENTRY_PARSER_NAME = "shard-follow-node-task-status-fetch-exceptions-entry"; - - static final ConstructingObjectParser, Void> FETCH_EXCEPTIONS_ENTRY_PARSER = - new ConstructingObjectParser<>( - FETCH_EXCEPTIONS_ENTRY_PARSER_NAME, - args -> new AbstractMap.SimpleEntry<>((long) args[0], (ElasticsearchException) args[1])); - - static { - STATUS_PARSER.declareString(ConstructingObjectParser.constructorArg(), LEADER_INDEX); - STATUS_PARSER.declareInt(ConstructingObjectParser.constructorArg(), SHARD_ID); - STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), LEADER_GLOBAL_CHECKPOINT_FIELD); - STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), LEADER_MAX_SEQ_NO_FIELD); - STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), FOLLOWER_GLOBAL_CHECKPOINT_FIELD); - STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), FOLLOWER_MAX_SEQ_NO_FIELD); - STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), LAST_REQUESTED_SEQ_NO_FIELD); - STATUS_PARSER.declareInt(ConstructingObjectParser.constructorArg(), NUMBER_OF_CONCURRENT_READS_FIELD); - STATUS_PARSER.declareInt(ConstructingObjectParser.constructorArg(), NUMBER_OF_CONCURRENT_WRITES_FIELD); - STATUS_PARSER.declareInt(ConstructingObjectParser.constructorArg(), NUMBER_OF_QUEUED_WRITES_FIELD); - STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), MAPPING_VERSION_FIELD); - STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), TOTAL_FETCH_TIME_MILLIS_FIELD); - STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), NUMBER_OF_SUCCESSFUL_FETCHES_FIELD); - STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), NUMBER_OF_FAILED_FETCHES_FIELD); - STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), OPERATIONS_RECEIVED_FIELD); - STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), TOTAL_TRANSFERRED_BYTES); - STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), TOTAL_INDEX_TIME_MILLIS_FIELD); - STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), NUMBER_OF_SUCCESSFUL_BULK_OPERATIONS_FIELD); - STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), NUMBER_OF_FAILED_BULK_OPERATIONS_FIELD); - STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), NUMBER_OF_OPERATIONS_INDEXED_FIELD); - STATUS_PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(), FETCH_EXCEPTIONS_ENTRY_PARSER, FETCH_EXCEPTIONS); - STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), TIME_SINCE_LAST_FETCH_MILLIS_FIELD); - } - - static final ParseField FETCH_EXCEPTIONS_ENTRY_FROM_SEQ_NO = new ParseField("from_seq_no"); - static final ParseField FETCH_EXCEPTIONS_ENTRY_EXCEPTION = new ParseField("exception"); - - static { - FETCH_EXCEPTIONS_ENTRY_PARSER.declareLong(ConstructingObjectParser.constructorArg(), FETCH_EXCEPTIONS_ENTRY_FROM_SEQ_NO); - FETCH_EXCEPTIONS_ENTRY_PARSER.declareObject( - ConstructingObjectParser.constructorArg(), - (p, c) -> ElasticsearchException.fromXContent(p), - FETCH_EXCEPTIONS_ENTRY_EXCEPTION); - } - - private final String leaderIndex; - - public String leaderIndex() { - return leaderIndex; - } - - private final int shardId; - - public int getShardId() { - return shardId; - } - - private final long leaderGlobalCheckpoint; - - public long leaderGlobalCheckpoint() { - return leaderGlobalCheckpoint; - } - - private final long leaderMaxSeqNo; - - public long leaderMaxSeqNo() { - return leaderMaxSeqNo; - } - - private final long followerGlobalCheckpoint; - - public long followerGlobalCheckpoint() { - return followerGlobalCheckpoint; - } - - private final long followerMaxSeqNo; - - public long followerMaxSeqNo() { - return followerMaxSeqNo; - } - - private final long lastRequestedSeqNo; - - public long lastRequestedSeqNo() { - return lastRequestedSeqNo; - } - - private final int numberOfConcurrentReads; - - public int numberOfConcurrentReads() { - return numberOfConcurrentReads; - } - - private final int numberOfConcurrentWrites; - - public int numberOfConcurrentWrites() { - return numberOfConcurrentWrites; - } - - private final int numberOfQueuedWrites; - - public int numberOfQueuedWrites() { - return numberOfQueuedWrites; - } - - private final long mappingVersion; - - public long mappingVersion() { - return mappingVersion; - } - - private final long totalFetchTimeMillis; - - public long totalFetchTimeMillis() { - return totalFetchTimeMillis; - } - - private final long numberOfSuccessfulFetches; - - public long numberOfSuccessfulFetches() { - return numberOfSuccessfulFetches; - } - - private final long numberOfFailedFetches; - - public long numberOfFailedFetches() { - return numberOfFailedFetches; - } - - private final long operationsReceived; - - public long operationsReceived() { - return operationsReceived; - } - - private final long totalTransferredBytes; - - public long totalTransferredBytes() { - return totalTransferredBytes; - } - - private final long totalIndexTimeMillis; - - public long totalIndexTimeMillis() { - return totalIndexTimeMillis; - } - - private final long numberOfSuccessfulBulkOperations; - - public long numberOfSuccessfulBulkOperations() { - return numberOfSuccessfulBulkOperations; - } - - private final long numberOfFailedBulkOperations; - - public long numberOfFailedBulkOperations() { - return numberOfFailedBulkOperations; - } - - private final long numberOfOperationsIndexed; - - public long numberOfOperationsIndexed() { - return numberOfOperationsIndexed; - } - - private final NavigableMap fetchExceptions; - - public NavigableMap fetchExceptions() { - return fetchExceptions; - } - - private final long timeSinceLastFetchMillis; - - public long timeSinceLastFetchMillis() { - return timeSinceLastFetchMillis; - } - - Status( - final String leaderIndex, - final int shardId, - final long leaderGlobalCheckpoint, - final long leaderMaxSeqNo, - final long followerGlobalCheckpoint, - final long followerMaxSeqNo, - final long lastRequestedSeqNo, - final int numberOfConcurrentReads, - final int numberOfConcurrentWrites, - final int numberOfQueuedWrites, - final long mappingVersion, - final long totalFetchTimeMillis, - final long numberOfSuccessfulFetches, - final long numberOfFailedFetches, - final long operationsReceived, - final long totalTransferredBytes, - final long totalIndexTimeMillis, - final long numberOfSuccessfulBulkOperations, - final long numberOfFailedBulkOperations, - final long numberOfOperationsIndexed, - final NavigableMap fetchExceptions, - final long timeSinceLastFetchMillis) { - this.leaderIndex = leaderIndex; - this.shardId = shardId; - this.leaderGlobalCheckpoint = leaderGlobalCheckpoint; - this.leaderMaxSeqNo = leaderMaxSeqNo; - this.followerGlobalCheckpoint = followerGlobalCheckpoint; - this.followerMaxSeqNo = followerMaxSeqNo; - this.lastRequestedSeqNo = lastRequestedSeqNo; - this.numberOfConcurrentReads = numberOfConcurrentReads; - this.numberOfConcurrentWrites = numberOfConcurrentWrites; - this.numberOfQueuedWrites = numberOfQueuedWrites; - this.mappingVersion = mappingVersion; - this.totalFetchTimeMillis = totalFetchTimeMillis; - this.numberOfSuccessfulFetches = numberOfSuccessfulFetches; - this.numberOfFailedFetches = numberOfFailedFetches; - this.operationsReceived = operationsReceived; - this.totalTransferredBytes = totalTransferredBytes; - this.totalIndexTimeMillis = totalIndexTimeMillis; - this.numberOfSuccessfulBulkOperations = numberOfSuccessfulBulkOperations; - this.numberOfFailedBulkOperations = numberOfFailedBulkOperations; - this.numberOfOperationsIndexed = numberOfOperationsIndexed; - this.fetchExceptions = Objects.requireNonNull(fetchExceptions); - this.timeSinceLastFetchMillis = timeSinceLastFetchMillis; - } - - public Status(final StreamInput in) throws IOException { - this.leaderIndex = in.readString(); - this.shardId = in.readVInt(); - this.leaderGlobalCheckpoint = in.readZLong(); - this.leaderMaxSeqNo = in.readZLong(); - this.followerGlobalCheckpoint = in.readZLong(); - this.followerMaxSeqNo = in.readZLong(); - this.lastRequestedSeqNo = in.readZLong(); - this.numberOfConcurrentReads = in.readVInt(); - this.numberOfConcurrentWrites = in.readVInt(); - this.numberOfQueuedWrites = in.readVInt(); - this.mappingVersion = in.readVLong(); - this.totalFetchTimeMillis = in.readVLong(); - this.numberOfSuccessfulFetches = in.readVLong(); - this.numberOfFailedFetches = in.readVLong(); - this.operationsReceived = in.readVLong(); - this.totalTransferredBytes = in.readVLong(); - this.totalIndexTimeMillis = in.readVLong(); - this.numberOfSuccessfulBulkOperations = in.readVLong(); - this.numberOfFailedBulkOperations = in.readVLong(); - this.numberOfOperationsIndexed = in.readVLong(); - this.fetchExceptions = new TreeMap<>(in.readMap(StreamInput::readVLong, StreamInput::readException)); - this.timeSinceLastFetchMillis = in.readZLong(); - } - - @Override - public String getWriteableName() { - return STATUS_PARSER_NAME; - } - - @Override - public void writeTo(final StreamOutput out) throws IOException { - out.writeString(leaderIndex); - out.writeVInt(shardId); - out.writeZLong(leaderGlobalCheckpoint); - out.writeZLong(leaderMaxSeqNo); - out.writeZLong(followerGlobalCheckpoint); - out.writeZLong(followerMaxSeqNo); - out.writeZLong(lastRequestedSeqNo); - out.writeVInt(numberOfConcurrentReads); - out.writeVInt(numberOfConcurrentWrites); - out.writeVInt(numberOfQueuedWrites); - out.writeVLong(mappingVersion); - out.writeVLong(totalFetchTimeMillis); - out.writeVLong(numberOfSuccessfulFetches); - out.writeVLong(numberOfFailedFetches); - out.writeVLong(operationsReceived); - out.writeVLong(totalTransferredBytes); - out.writeVLong(totalIndexTimeMillis); - out.writeVLong(numberOfSuccessfulBulkOperations); - out.writeVLong(numberOfFailedBulkOperations); - out.writeVLong(numberOfOperationsIndexed); - out.writeMap(fetchExceptions, StreamOutput::writeVLong, StreamOutput::writeException); - out.writeZLong(timeSinceLastFetchMillis); - } - - @Override - public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException { - builder.startObject(); - { - builder.field(LEADER_INDEX.getPreferredName(), leaderIndex); - builder.field(SHARD_ID.getPreferredName(), shardId); - builder.field(LEADER_GLOBAL_CHECKPOINT_FIELD.getPreferredName(), leaderGlobalCheckpoint); - builder.field(LEADER_MAX_SEQ_NO_FIELD.getPreferredName(), leaderMaxSeqNo); - builder.field(FOLLOWER_GLOBAL_CHECKPOINT_FIELD.getPreferredName(), followerGlobalCheckpoint); - builder.field(FOLLOWER_MAX_SEQ_NO_FIELD.getPreferredName(), followerMaxSeqNo); - builder.field(LAST_REQUESTED_SEQ_NO_FIELD.getPreferredName(), lastRequestedSeqNo); - builder.field(NUMBER_OF_CONCURRENT_READS_FIELD.getPreferredName(), numberOfConcurrentReads); - builder.field(NUMBER_OF_CONCURRENT_WRITES_FIELD.getPreferredName(), numberOfConcurrentWrites); - builder.field(NUMBER_OF_QUEUED_WRITES_FIELD.getPreferredName(), numberOfQueuedWrites); - builder.field(MAPPING_VERSION_FIELD.getPreferredName(), mappingVersion); - builder.humanReadableField( - TOTAL_FETCH_TIME_MILLIS_FIELD.getPreferredName(), - "total_fetch_time", - new TimeValue(totalFetchTimeMillis, TimeUnit.MILLISECONDS)); - builder.field(NUMBER_OF_SUCCESSFUL_FETCHES_FIELD.getPreferredName(), numberOfSuccessfulFetches); - builder.field(NUMBER_OF_FAILED_FETCHES_FIELD.getPreferredName(), numberOfFailedFetches); - builder.field(OPERATIONS_RECEIVED_FIELD.getPreferredName(), operationsReceived); - builder.humanReadableField( - TOTAL_TRANSFERRED_BYTES.getPreferredName(), - "total_transferred", - new ByteSizeValue(totalTransferredBytes, ByteSizeUnit.BYTES)); - builder.humanReadableField( - TOTAL_INDEX_TIME_MILLIS_FIELD.getPreferredName(), - "total_index_time", - new TimeValue(totalIndexTimeMillis, TimeUnit.MILLISECONDS)); - builder.field(NUMBER_OF_SUCCESSFUL_BULK_OPERATIONS_FIELD.getPreferredName(), numberOfSuccessfulBulkOperations); - builder.field(NUMBER_OF_FAILED_BULK_OPERATIONS_FIELD.getPreferredName(), numberOfFailedBulkOperations); - builder.field(NUMBER_OF_OPERATIONS_INDEXED_FIELD.getPreferredName(), numberOfOperationsIndexed); - builder.startArray(FETCH_EXCEPTIONS.getPreferredName()); - { - for (final Map.Entry entry : fetchExceptions.entrySet()) { - builder.startObject(); - { - builder.field(FETCH_EXCEPTIONS_ENTRY_FROM_SEQ_NO.getPreferredName(), entry.getKey()); - builder.field(FETCH_EXCEPTIONS_ENTRY_EXCEPTION.getPreferredName()); - builder.startObject(); - { - ElasticsearchException.generateThrowableXContent(builder, params, entry.getValue()); - } - builder.endObject(); - } - builder.endObject(); - } - } - builder.endArray(); - builder.humanReadableField( - TIME_SINCE_LAST_FETCH_MILLIS_FIELD.getPreferredName(), - "time_since_last_fetch", - new TimeValue(timeSinceLastFetchMillis, TimeUnit.MILLISECONDS)); - } - builder.endObject(); - return builder; - } - - public static Status fromXContent(final XContentParser parser) { - return STATUS_PARSER.apply(parser, null); - } - - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - final Status that = (Status) o; - return leaderIndex.equals(that.leaderIndex) && - shardId == that.shardId && - leaderGlobalCheckpoint == that.leaderGlobalCheckpoint && - leaderMaxSeqNo == that.leaderMaxSeqNo && - followerGlobalCheckpoint == that.followerGlobalCheckpoint && - followerMaxSeqNo == that.followerMaxSeqNo && - lastRequestedSeqNo == that.lastRequestedSeqNo && - numberOfConcurrentReads == that.numberOfConcurrentReads && - numberOfConcurrentWrites == that.numberOfConcurrentWrites && - numberOfQueuedWrites == that.numberOfQueuedWrites && - mappingVersion == that.mappingVersion && - totalFetchTimeMillis == that.totalFetchTimeMillis && - numberOfSuccessfulFetches == that.numberOfSuccessfulFetches && - numberOfFailedFetches == that.numberOfFailedFetches && - operationsReceived == that.operationsReceived && - totalTransferredBytes == that.totalTransferredBytes && - numberOfSuccessfulBulkOperations == that.numberOfSuccessfulBulkOperations && - numberOfFailedBulkOperations == that.numberOfFailedBulkOperations && - numberOfOperationsIndexed == that.numberOfOperationsIndexed && - /* - * ElasticsearchException does not implement equals so we will assume the fetch exceptions are equal if they are equal - * up to the key set and their messages. Note that we are relying on the fact that the fetch exceptions are ordered by - * keys. - */ - fetchExceptions.keySet().equals(that.fetchExceptions.keySet()) && - getFetchExceptionMessages(this).equals(getFetchExceptionMessages(that)) && - timeSinceLastFetchMillis == that.timeSinceLastFetchMillis; - } - - @Override - public int hashCode() { - return Objects.hash( - leaderIndex, - shardId, - leaderGlobalCheckpoint, - leaderMaxSeqNo, - followerGlobalCheckpoint, - followerMaxSeqNo, - lastRequestedSeqNo, - numberOfConcurrentReads, - numberOfConcurrentWrites, - numberOfQueuedWrites, - mappingVersion, - totalFetchTimeMillis, - numberOfSuccessfulFetches, - numberOfFailedFetches, - operationsReceived, - totalTransferredBytes, - numberOfSuccessfulBulkOperations, - numberOfFailedBulkOperations, - numberOfOperationsIndexed, - /* - * ElasticsearchException does not implement hash code so we will compute the hash code based on the key set and the - * messages. Note that we are relying on the fact that the fetch exceptions are ordered by keys. - */ - fetchExceptions.keySet(), - getFetchExceptionMessages(this), - timeSinceLastFetchMillis); - } - - private static List getFetchExceptionMessages(final Status status) { - return status.fetchExceptions().values().stream().map(ElasticsearchException::getMessage).collect(Collectors.toList()); - } - - public String toString() { - return Strings.toString(this); - } - - } - } diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportCcrStatsAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportCcrStatsAction.java index 3b5d0ac53cf..d4425773fa1 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportCcrStatsAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportCcrStatsAction.java @@ -22,6 +22,7 @@ import org.elasticsearch.tasks.Task; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.ccr.Ccr; import org.elasticsearch.xpack.ccr.CcrLicenseChecker; +import org.elasticsearch.xpack.core.ccr.action.CcrStatsAction; import java.io.IOException; import java.util.Arrays; diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportCreateAndFollowIndexAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportCreateAndFollowIndexAction.java new file mode 100644 index 00000000000..b99b569a525 --- /dev/null +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportCreateAndFollowIndexAction.java @@ -0,0 +1,231 @@ +/* + * 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.ccr.action; + +import com.carrotsearch.hppc.cursors.ObjectObjectCursor; +import org.elasticsearch.ResourceAlreadyExistsException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.ActiveShardCount; +import org.elasticsearch.action.support.ActiveShardsObserver; +import org.elasticsearch.action.support.master.TransportMasterNodeAction; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.AckedClusterStateUpdateTask; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.block.ClusterBlockException; +import org.elasticsearch.cluster.block.ClusterBlockLevel; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.metadata.MappingMetaData; +import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.cluster.routing.RoutingTable; +import org.elasticsearch.cluster.routing.allocation.AllocationService; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.UUIDs; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.license.LicenseUtils; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.RemoteClusterAware; +import org.elasticsearch.transport.RemoteClusterService; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.ccr.CcrLicenseChecker; +import org.elasticsearch.xpack.ccr.CcrSettings; +import org.elasticsearch.xpack.core.ccr.action.CreateAndFollowIndexAction; +import org.elasticsearch.xpack.core.ccr.action.FollowIndexAction; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public final class TransportCreateAndFollowIndexAction + extends TransportMasterNodeAction { + + private final Client client; + private final AllocationService allocationService; + private final RemoteClusterService remoteClusterService; + private final ActiveShardsObserver activeShardsObserver; + private final CcrLicenseChecker ccrLicenseChecker; + + @Inject + public TransportCreateAndFollowIndexAction( + final Settings settings, + final ThreadPool threadPool, + final TransportService transportService, + final ClusterService clusterService, + final ActionFilters actionFilters, + final IndexNameExpressionResolver indexNameExpressionResolver, + final Client client, + final AllocationService allocationService, + final CcrLicenseChecker ccrLicenseChecker) { + super( + settings, + CreateAndFollowIndexAction.NAME, + transportService, + clusterService, + threadPool, + actionFilters, + indexNameExpressionResolver, + CreateAndFollowIndexAction.Request::new); + this.client = client; + this.allocationService = allocationService; + this.remoteClusterService = transportService.getRemoteClusterService(); + this.activeShardsObserver = new ActiveShardsObserver(settings, clusterService, threadPool); + this.ccrLicenseChecker = Objects.requireNonNull(ccrLicenseChecker); + } + + @Override + protected String executor() { + return ThreadPool.Names.SAME; + } + + @Override + protected CreateAndFollowIndexAction.Response newResponse() { + return new CreateAndFollowIndexAction.Response(); + } + + @Override + protected void masterOperation( + final CreateAndFollowIndexAction.Request request, + final ClusterState state, + final ActionListener listener) throws Exception { + if (ccrLicenseChecker.isCcrAllowed() == false) { + listener.onFailure(LicenseUtils.newComplianceException("ccr")); + return; + } + final String[] indices = new String[]{request.getFollowRequest().getLeaderIndex()}; + final Map> remoteClusterIndices = remoteClusterService.groupClusterIndices(indices, s -> false); + if (remoteClusterIndices.containsKey(RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY)) { + createFollowerIndexAndFollowLocalIndex(request, state, listener); + } else { + assert remoteClusterIndices.size() == 1; + final Map.Entry> entry = remoteClusterIndices.entrySet().iterator().next(); + assert entry.getValue().size() == 1; + final String clusterAlias = entry.getKey(); + final String leaderIndex = entry.getValue().get(0); + createFollowerIndexAndFollowRemoteIndex(request, clusterAlias, leaderIndex, listener); + } + } + + private void createFollowerIndexAndFollowLocalIndex( + final CreateAndFollowIndexAction.Request request, + final ClusterState state, + final ActionListener listener) { + // following an index in local cluster, so use local cluster state to fetch leader index metadata + final IndexMetaData leaderIndexMetadata = state.getMetaData().index(request.getFollowRequest().getLeaderIndex()); + createFollowerIndex(leaderIndexMetadata, request, listener); + } + + private void createFollowerIndexAndFollowRemoteIndex( + final CreateAndFollowIndexAction.Request request, + final String clusterAlias, + final String leaderIndex, + final ActionListener listener) { + ccrLicenseChecker.checkRemoteClusterLicenseAndFetchLeaderIndexMetadata( + client, + clusterAlias, + leaderIndex, + listener::onFailure, + leaderIndexMetaData -> createFollowerIndex(leaderIndexMetaData, request, listener)); + } + + private void createFollowerIndex( + final IndexMetaData leaderIndexMetaData, + final CreateAndFollowIndexAction.Request request, + final ActionListener listener) { + if (leaderIndexMetaData == null) { + listener.onFailure(new IllegalArgumentException("leader index [" + request.getFollowRequest().getLeaderIndex() + + "] does not exist")); + return; + } + + ActionListener handler = ActionListener.wrap( + result -> { + if (result) { + initiateFollowing(request, listener); + } else { + listener.onResponse(new CreateAndFollowIndexAction.Response(true, false, false)); + } + }, + listener::onFailure); + // Can't use create index api here, because then index templates can alter the mappings / settings. + // And index templates could introduce settings / mappings that are incompatible with the leader index. + clusterService.submitStateUpdateTask("follow_index_action", new AckedClusterStateUpdateTask(request, handler) { + + @Override + protected Boolean newResponse(final boolean acknowledged) { + return acknowledged; + } + + @Override + public ClusterState execute(final ClusterState currentState) throws Exception { + String followIndex = request.getFollowRequest().getFollowerIndex(); + IndexMetaData currentIndex = currentState.metaData().index(followIndex); + if (currentIndex != null) { + throw new ResourceAlreadyExistsException(currentIndex.getIndex()); + } + + MetaData.Builder mdBuilder = MetaData.builder(currentState.metaData()); + IndexMetaData.Builder imdBuilder = IndexMetaData.builder(followIndex); + + // Copy all settings, but overwrite a few settings. + Settings.Builder settingsBuilder = Settings.builder(); + settingsBuilder.put(leaderIndexMetaData.getSettings()); + // Overwriting UUID here, because otherwise we can't follow indices in the same cluster + settingsBuilder.put(IndexMetaData.SETTING_INDEX_UUID, UUIDs.randomBase64UUID()); + settingsBuilder.put(IndexMetaData.SETTING_INDEX_PROVIDED_NAME, followIndex); + settingsBuilder.put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true); + imdBuilder.settings(settingsBuilder); + + // Copy mappings from leader IMD to follow IMD + for (ObjectObjectCursor cursor : leaderIndexMetaData.getMappings()) { + imdBuilder.putMapping(cursor.value); + } + imdBuilder.setRoutingNumShards(leaderIndexMetaData.getRoutingNumShards()); + IndexMetaData followIMD = imdBuilder.build(); + mdBuilder.put(followIMD, false); + + ClusterState.Builder builder = ClusterState.builder(currentState); + builder.metaData(mdBuilder.build()); + ClusterState updatedState = builder.build(); + + RoutingTable.Builder routingTableBuilder = RoutingTable.builder(updatedState.routingTable()) + .addAsNew(updatedState.metaData().index(request.getFollowRequest().getFollowerIndex())); + updatedState = allocationService.reroute( + ClusterState.builder(updatedState).routingTable(routingTableBuilder.build()).build(), + "follow index [" + request.getFollowRequest().getFollowerIndex() + "] created"); + + logger.info("[{}] creating index, cause [ccr_create_and_follow], shards [{}]/[{}]", + followIndex, followIMD.getNumberOfShards(), followIMD.getNumberOfReplicas()); + + return updatedState; + } + }); + } + + private void initiateFollowing( + final CreateAndFollowIndexAction.Request request, + final ActionListener listener) { + activeShardsObserver.waitForActiveShards(new String[]{request.getFollowRequest().getFollowerIndex()}, + ActiveShardCount.DEFAULT, request.timeout(), result -> { + if (result) { + client.execute(FollowIndexAction.INSTANCE, request.getFollowRequest(), ActionListener.wrap( + r -> listener.onResponse(new CreateAndFollowIndexAction.Response(true, true, r.isAcknowledged())), + listener::onFailure + )); + } else { + listener.onResponse(new CreateAndFollowIndexAction.Response(true, false, false)); + } + }, listener::onFailure); + } + + @Override + protected ClusterBlockException checkBlock(final CreateAndFollowIndexAction.Request request, final ClusterState state) { + return state.blocks().indexBlockedException(ClusterBlockLevel.METADATA_WRITE, request.getFollowRequest().getFollowerIndex()); + } + +} diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexAction.java new file mode 100644 index 00000000000..33447ef4208 --- /dev/null +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexAction.java @@ -0,0 +1,336 @@ +/* + * 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.ccr.action; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.cluster.routing.allocation.decider.EnableAllocationDecider; +import org.elasticsearch.cluster.routing.allocation.decider.ShardsLimitAllocationDecider; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.IndexingSlowLog; +import org.elasticsearch.index.SearchSlowLog; +import org.elasticsearch.index.cache.bitset.BitsetFilterCache; +import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.indices.IndicesRequestCache; +import org.elasticsearch.indices.IndicesService; +import org.elasticsearch.license.LicenseUtils; +import org.elasticsearch.persistent.PersistentTasksCustomMetaData; +import org.elasticsearch.persistent.PersistentTasksService; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.RemoteClusterAware; +import org.elasticsearch.transport.RemoteClusterService; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.ccr.CcrLicenseChecker; +import org.elasticsearch.xpack.ccr.CcrSettings; +import org.elasticsearch.xpack.core.ccr.action.FollowIndexAction; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReferenceArray; +import java.util.stream.Collectors; + +public class TransportFollowIndexAction extends HandledTransportAction { + + private final Client client; + private final ThreadPool threadPool; + private final ClusterService clusterService; + private final RemoteClusterService remoteClusterService; + private final PersistentTasksService persistentTasksService; + private final IndicesService indicesService; + private final CcrLicenseChecker ccrLicenseChecker; + + @Inject + public TransportFollowIndexAction( + final Settings settings, + final ThreadPool threadPool, + final TransportService transportService, + final ActionFilters actionFilters, + final Client client, + final ClusterService clusterService, + final PersistentTasksService persistentTasksService, + final IndicesService indicesService, + final CcrLicenseChecker ccrLicenseChecker) { + super(settings, FollowIndexAction.NAME, transportService, actionFilters, FollowIndexAction.Request::new); + this.client = client; + this.threadPool = threadPool; + this.clusterService = clusterService; + this.remoteClusterService = transportService.getRemoteClusterService(); + this.persistentTasksService = persistentTasksService; + this.indicesService = indicesService; + this.ccrLicenseChecker = Objects.requireNonNull(ccrLicenseChecker); + } + + @Override + protected void doExecute(final Task task, + final FollowIndexAction.Request request, + final ActionListener listener) { + if (ccrLicenseChecker.isCcrAllowed() == false) { + listener.onFailure(LicenseUtils.newComplianceException("ccr")); + return; + } + final String[] indices = new String[]{request.getLeaderIndex()}; + final Map> remoteClusterIndices = remoteClusterService.groupClusterIndices(indices, s -> false); + if (remoteClusterIndices.containsKey(RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY)) { + followLocalIndex(request, listener); + } else { + assert remoteClusterIndices.size() == 1; + final Map.Entry> entry = remoteClusterIndices.entrySet().iterator().next(); + assert entry.getValue().size() == 1; + final String clusterAlias = entry.getKey(); + final String leaderIndex = entry.getValue().get(0); + followRemoteIndex(request, clusterAlias, leaderIndex, listener); + } + } + + private void followLocalIndex(final FollowIndexAction.Request request, + final ActionListener listener) { + final ClusterState state = clusterService.state(); + final IndexMetaData followerIndexMetadata = state.getMetaData().index(request.getFollowerIndex()); + // following an index in local cluster, so use local cluster state to fetch leader index metadata + final IndexMetaData leaderIndexMetadata = state.getMetaData().index(request.getLeaderIndex()); + try { + start(request, null, leaderIndexMetadata, followerIndexMetadata, listener); + } catch (final IOException e) { + listener.onFailure(e); + } + } + + private void followRemoteIndex( + final FollowIndexAction.Request request, + final String clusterAlias, + final String leaderIndex, + final ActionListener listener) { + final ClusterState state = clusterService.state(); + final IndexMetaData followerIndexMetadata = state.getMetaData().index(request.getFollowerIndex()); + ccrLicenseChecker.checkRemoteClusterLicenseAndFetchLeaderIndexMetadata( + client, + clusterAlias, + leaderIndex, + listener::onFailure, + leaderIndexMetadata -> { + try { + start(request, clusterAlias, leaderIndexMetadata, followerIndexMetadata, listener); + } catch (final IOException e) { + listener.onFailure(e); + } + }); + } + + /** + * Performs validation on the provided leader and follow {@link IndexMetaData} instances and then + * creates a persistent task for each leader primary shard. This persistent tasks track changes in the leader + * shard and replicate these changes to a follower shard. + * + * Currently the following validation is performed: + *
    + *
  • The leader index and follow index need to have the same number of primary shards
  • + *
+ */ + void start( + FollowIndexAction.Request request, + String clusterNameAlias, + IndexMetaData leaderIndexMetadata, + IndexMetaData followIndexMetadata, + ActionListener handler) throws IOException { + + MapperService mapperService = followIndexMetadata != null ? indicesService.createIndexMapperService(followIndexMetadata) : null; + validate(request, leaderIndexMetadata, followIndexMetadata, mapperService); + final int numShards = followIndexMetadata.getNumberOfShards(); + final AtomicInteger counter = new AtomicInteger(numShards); + final AtomicReferenceArray responses = new AtomicReferenceArray<>(followIndexMetadata.getNumberOfShards()); + Map filteredHeaders = threadPool.getThreadContext().getHeaders().entrySet().stream() + .filter(e -> ShardFollowTask.HEADER_FILTERS.contains(e.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));for (int i = 0; i < numShards; i++) { + final int shardId = i; + String taskId = followIndexMetadata.getIndexUUID() + "-" + shardId; + + ShardFollowTask shardFollowTask = new ShardFollowTask( + clusterNameAlias, + new ShardId(followIndexMetadata.getIndex(), shardId), + new ShardId(leaderIndexMetadata.getIndex(), shardId), + request.getMaxBatchOperationCount(), + request.getMaxConcurrentReadBatches(), + request.getMaxOperationSizeInBytes(), + request.getMaxConcurrentWriteBatches(), + request.getMaxWriteBufferSize(), + request.getRetryTimeout(), + request.getIdleShardRetryDelay(), + filteredHeaders); + persistentTasksService.sendStartRequest(taskId, ShardFollowTask.NAME, shardFollowTask, + new ActionListener>() { + @Override + public void onResponse(PersistentTasksCustomMetaData.PersistentTask task) { + responses.set(shardId, task); + finalizeResponse(); + } + + @Override + public void onFailure(Exception e) { + responses.set(shardId, e); + finalizeResponse(); + } + + void finalizeResponse() { + Exception error = null; + if (counter.decrementAndGet() == 0) { + for (int j = 0; j < responses.length(); j++) { + Object response = responses.get(j); + if (response instanceof Exception) { + if (error == null) { + error = (Exception) response; + } else { + error.addSuppressed((Throwable) response); + } + } + } + + if (error == null) { + // include task ids? + handler.onResponse(new AcknowledgedResponse(true)); + } else { + // TODO: cancel all started tasks + handler.onFailure(error); + } + } + } + } + ); + } + } + + static void validate( + final FollowIndexAction.Request request, + final IndexMetaData leaderIndex, + final IndexMetaData followIndex, + final MapperService followerMapperService) { + if (leaderIndex == null) { + throw new IllegalArgumentException("leader index [" + request.getLeaderIndex() + "] does not exist"); + } + if (followIndex == null) { + throw new IllegalArgumentException("follow index [" + request.getFollowerIndex() + "] does not exist"); + } + if (leaderIndex.getSettings().getAsBoolean(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), false) == false) { + throw new IllegalArgumentException("leader index [" + request.getLeaderIndex() + "] does not have soft deletes enabled"); + } + if (leaderIndex.getNumberOfShards() != followIndex.getNumberOfShards()) { + throw new IllegalArgumentException("leader index primary shards [" + leaderIndex.getNumberOfShards() + + "] does not match with the number of shards of the follow index [" + followIndex.getNumberOfShards() + "]"); + } + if (leaderIndex.getRoutingNumShards() != followIndex.getRoutingNumShards()) { + throw new IllegalArgumentException("leader index number_of_routing_shards [" + leaderIndex.getRoutingNumShards() + + "] does not match with the number_of_routing_shards of the follow index [" + followIndex.getRoutingNumShards() + "]"); + } + if (leaderIndex.getState() != IndexMetaData.State.OPEN || followIndex.getState() != IndexMetaData.State.OPEN) { + throw new IllegalArgumentException("leader and follow index must be open"); + } + if (CcrSettings.CCR_FOLLOWING_INDEX_SETTING.get(followIndex.getSettings()) == false) { + throw new IllegalArgumentException("the following index [" + request.getFollowerIndex() + "] is not ready " + + "to follow; the setting [" + CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey() + "] must be enabled."); + } + // Make a copy, remove settings that are allowed to be different and then compare if the settings are equal. + Settings leaderSettings = filter(leaderIndex.getSettings()); + Settings followerSettings = filter(followIndex.getSettings()); + if (leaderSettings.equals(followerSettings) == false) { + throw new IllegalArgumentException("the leader and follower index settings must be identical"); + } + + // Validates if the current follower mapping is mergable with the leader mapping. + // This also validates for example whether specific mapper plugins have been installed + followerMapperService.merge(leaderIndex, MapperService.MergeReason.MAPPING_RECOVERY); + } + + private static final Set> WHITE_LISTED_SETTINGS; + + static { + final Set> whiteListedSettings = new HashSet<>(); + whiteListedSettings.add(IndexMetaData.INDEX_NUMBER_OF_REPLICAS_SETTING); + whiteListedSettings.add(IndexMetaData.INDEX_AUTO_EXPAND_REPLICAS_SETTING); + + whiteListedSettings.add(IndexMetaData.INDEX_ROUTING_EXCLUDE_GROUP_SETTING); + whiteListedSettings.add(IndexMetaData.INDEX_ROUTING_INCLUDE_GROUP_SETTING); + whiteListedSettings.add(IndexMetaData.INDEX_ROUTING_REQUIRE_GROUP_SETTING); + whiteListedSettings.add(EnableAllocationDecider.INDEX_ROUTING_REBALANCE_ENABLE_SETTING); + whiteListedSettings.add(EnableAllocationDecider.INDEX_ROUTING_ALLOCATION_ENABLE_SETTING); + whiteListedSettings.add(ShardsLimitAllocationDecider.INDEX_TOTAL_SHARDS_PER_NODE_SETTING); + + whiteListedSettings.add(IndicesRequestCache.INDEX_CACHE_REQUEST_ENABLED_SETTING); + whiteListedSettings.add(IndexSettings.MAX_RESULT_WINDOW_SETTING); + whiteListedSettings.add(IndexSettings.INDEX_WARMER_ENABLED_SETTING); + whiteListedSettings.add(IndexSettings.INDEX_REFRESH_INTERVAL_SETTING); + whiteListedSettings.add(IndexSettings.MAX_RESCORE_WINDOW_SETTING); + whiteListedSettings.add(IndexSettings.MAX_INNER_RESULT_WINDOW_SETTING); + whiteListedSettings.add(IndexSettings.DEFAULT_FIELD_SETTING); + whiteListedSettings.add(IndexSettings.QUERY_STRING_LENIENT_SETTING); + whiteListedSettings.add(IndexSettings.QUERY_STRING_ANALYZE_WILDCARD); + whiteListedSettings.add(IndexSettings.QUERY_STRING_ALLOW_LEADING_WILDCARD); + whiteListedSettings.add(IndexSettings.ALLOW_UNMAPPED); + whiteListedSettings.add(IndexSettings.INDEX_SEARCH_IDLE_AFTER); + whiteListedSettings.add(BitsetFilterCache.INDEX_LOAD_RANDOM_ACCESS_FILTERS_EAGERLY_SETTING); + + whiteListedSettings.add(SearchSlowLog.INDEX_SEARCH_SLOWLOG_THRESHOLD_FETCH_DEBUG_SETTING); + whiteListedSettings.add(SearchSlowLog.INDEX_SEARCH_SLOWLOG_THRESHOLD_FETCH_WARN_SETTING); + whiteListedSettings.add(SearchSlowLog.INDEX_SEARCH_SLOWLOG_THRESHOLD_FETCH_INFO_SETTING); + whiteListedSettings.add(SearchSlowLog.INDEX_SEARCH_SLOWLOG_THRESHOLD_FETCH_TRACE_SETTING); + whiteListedSettings.add(SearchSlowLog.INDEX_SEARCH_SLOWLOG_THRESHOLD_QUERY_WARN_SETTING); + whiteListedSettings.add(SearchSlowLog.INDEX_SEARCH_SLOWLOG_THRESHOLD_QUERY_DEBUG_SETTING); + whiteListedSettings.add(SearchSlowLog.INDEX_SEARCH_SLOWLOG_THRESHOLD_QUERY_INFO_SETTING); + whiteListedSettings.add(SearchSlowLog.INDEX_SEARCH_SLOWLOG_THRESHOLD_QUERY_TRACE_SETTING); + whiteListedSettings.add(SearchSlowLog.INDEX_SEARCH_SLOWLOG_LEVEL); + whiteListedSettings.add(IndexingSlowLog.INDEX_INDEXING_SLOWLOG_THRESHOLD_INDEX_WARN_SETTING); + whiteListedSettings.add(IndexingSlowLog.INDEX_INDEXING_SLOWLOG_THRESHOLD_INDEX_DEBUG_SETTING); + whiteListedSettings.add(IndexingSlowLog.INDEX_INDEXING_SLOWLOG_THRESHOLD_INDEX_INFO_SETTING); + whiteListedSettings.add(IndexingSlowLog.INDEX_INDEXING_SLOWLOG_THRESHOLD_INDEX_TRACE_SETTING); + whiteListedSettings.add(IndexingSlowLog.INDEX_INDEXING_SLOWLOG_LEVEL_SETTING); + whiteListedSettings.add(IndexingSlowLog.INDEX_INDEXING_SLOWLOG_REFORMAT_SETTING); + whiteListedSettings.add(IndexingSlowLog.INDEX_INDEXING_SLOWLOG_MAX_SOURCE_CHARS_TO_LOG_SETTING); + + whiteListedSettings.add(IndexSettings.INDEX_SOFT_DELETES_SETTING); + whiteListedSettings.add(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING); + + WHITE_LISTED_SETTINGS = Collections.unmodifiableSet(whiteListedSettings); + } + + private static Settings filter(Settings originalSettings) { + Settings.Builder settings = Settings.builder().put(originalSettings); + // Remove settings that are always going to be different between leader and follow index: + settings.remove(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey()); + settings.remove(IndexMetaData.SETTING_INDEX_UUID); + settings.remove(IndexMetaData.SETTING_INDEX_PROVIDED_NAME); + settings.remove(IndexMetaData.SETTING_CREATION_DATE); + + Iterator iterator = settings.keys().iterator(); + while (iterator.hasNext()) { + String key = iterator.next(); + for (Setting whitelistedSetting : WHITE_LISTED_SETTINGS) { + if (whitelistedSetting.match(key)) { + iterator.remove(); + break; + } + } + } + return settings.build(); + } + +} diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportUnfollowIndexAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportUnfollowIndexAction.java new file mode 100644 index 00000000000..05cde0eab85 --- /dev/null +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportUnfollowIndexAction.java @@ -0,0 +1,105 @@ +/* + * 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.ccr.action; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.admin.cluster.state.ClusterStateRequest; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.persistent.PersistentTasksCustomMetaData; +import org.elasticsearch.persistent.PersistentTasksService; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.ccr.action.UnfollowIndexAction; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReferenceArray; + +public class TransportUnfollowIndexAction extends HandledTransportAction { + + private final Client client; + private final PersistentTasksService persistentTasksService; + + @Inject + public TransportUnfollowIndexAction( + final Settings settings, + final TransportService transportService, + final ActionFilters actionFilters, + final Client client, + final PersistentTasksService persistentTasksService) { + super(settings, UnfollowIndexAction.NAME, transportService, actionFilters, UnfollowIndexAction.Request::new); + this.client = client; + this.persistentTasksService = persistentTasksService; + } + + @Override + protected void doExecute( + final Task task, + final UnfollowIndexAction.Request request, + final ActionListener listener) { + + client.admin().cluster().state(new ClusterStateRequest(), ActionListener.wrap(r -> { + IndexMetaData followIndexMetadata = r.getState().getMetaData().index(request.getFollowIndex()); + if (followIndexMetadata == null) { + listener.onFailure(new IllegalArgumentException("follow index [" + request.getFollowIndex() + "] does not exist")); + return; + } + + final int numShards = followIndexMetadata.getNumberOfShards(); + final AtomicInteger counter = new AtomicInteger(numShards); + final AtomicReferenceArray responses = new AtomicReferenceArray<>(followIndexMetadata.getNumberOfShards()); + for (int i = 0; i < numShards; i++) { + final int shardId = i; + String taskId = followIndexMetadata.getIndexUUID() + "-" + shardId; + persistentTasksService.sendRemoveRequest(taskId, + new ActionListener>() { + @Override + public void onResponse(PersistentTasksCustomMetaData.PersistentTask task) { + responses.set(shardId, task); + finalizeResponse(); + } + + @Override + public void onFailure(Exception e) { + responses.set(shardId, e); + finalizeResponse(); + } + + void finalizeResponse() { + Exception error = null; + if (counter.decrementAndGet() == 0) { + for (int j = 0; j < responses.length(); j++) { + Object response = responses.get(j); + if (response instanceof Exception) { + if (error == null) { + error = (Exception) response; + } else { + error.addSuppressed((Throwable) response); + } + } + } + + if (error == null) { + // include task ids? + listener.onResponse(new AcknowledgedResponse(true)); + } else { + // TODO: cancel all started tasks + listener.onFailure(error); + } + } + } + }); + } + }, listener::onFailure)); + } + +} \ No newline at end of file diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/UnfollowIndexAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/UnfollowIndexAction.java deleted file mode 100644 index 93b2bcc3e40..00000000000 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/UnfollowIndexAction.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.xpack.ccr.action; - -import org.elasticsearch.action.Action; -import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.ActionRequest; -import org.elasticsearch.action.ActionRequestValidationException; -import org.elasticsearch.action.admin.cluster.state.ClusterStateRequest; -import org.elasticsearch.action.support.ActionFilters; -import org.elasticsearch.action.support.HandledTransportAction; -import org.elasticsearch.action.support.master.AcknowledgedResponse; -import org.elasticsearch.client.Client; -import org.elasticsearch.cluster.metadata.IndexMetaData; -import org.elasticsearch.common.inject.Inject; -import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.persistent.PersistentTasksCustomMetaData; -import org.elasticsearch.persistent.PersistentTasksService; -import org.elasticsearch.tasks.Task; -import org.elasticsearch.transport.TransportService; - -import java.io.IOException; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReferenceArray; - -public class UnfollowIndexAction extends Action { - - public static final UnfollowIndexAction INSTANCE = new UnfollowIndexAction(); - public static final String NAME = "cluster:admin/xpack/ccr/unfollow_index"; - - private UnfollowIndexAction() { - super(NAME); - } - - @Override - public AcknowledgedResponse newResponse() { - return new AcknowledgedResponse(); - } - - public static class Request extends ActionRequest { - - private String followIndex; - - public String getFollowIndex() { - return followIndex; - } - - public void setFollowIndex(String followIndex) { - this.followIndex = followIndex; - } - - @Override - public ActionRequestValidationException validate() { - return null; - } - - @Override - public void readFrom(StreamInput in) throws IOException { - super.readFrom(in); - followIndex = in.readString(); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - super.writeTo(out); - out.writeString(followIndex); - } - } - - public static class TransportAction extends HandledTransportAction { - - private final Client client; - private final PersistentTasksService persistentTasksService; - - @Inject - public TransportAction(Settings settings, - TransportService transportService, - ActionFilters actionFilters, - Client client, - PersistentTasksService persistentTasksService) { - super(settings, NAME, transportService, actionFilters, Request::new); - this.client = client; - this.persistentTasksService = persistentTasksService; - } - - @Override - protected void doExecute(Task task, - Request request, - ActionListener listener) { - - client.admin().cluster().state(new ClusterStateRequest(), ActionListener.wrap(r -> { - IndexMetaData followIndexMetadata = r.getState().getMetaData().index(request.followIndex); - if (followIndexMetadata == null) { - listener.onFailure(new IllegalArgumentException("follow index [" + request.followIndex + "] does not exist")); - return; - } - - final int numShards = followIndexMetadata.getNumberOfShards(); - final AtomicInteger counter = new AtomicInteger(numShards); - final AtomicReferenceArray responses = new AtomicReferenceArray<>(followIndexMetadata.getNumberOfShards()); - for (int i = 0; i < numShards; i++) { - final int shardId = i; - String taskId = followIndexMetadata.getIndexUUID() + "-" + shardId; - persistentTasksService.sendRemoveRequest(taskId, - new ActionListener>() { - @Override - public void onResponse(PersistentTasksCustomMetaData.PersistentTask task) { - responses.set(shardId, task); - finalizeResponse(); - } - - @Override - public void onFailure(Exception e) { - responses.set(shardId, e); - finalizeResponse(); - } - - void finalizeResponse() { - Exception error = null; - if (counter.decrementAndGet() == 0) { - for (int j = 0; j < responses.length(); j++) { - Object response = responses.get(j); - if (response instanceof Exception) { - if (error == null) { - error = (Exception) response; - } else { - error.addSuppressed((Throwable) response); - } - } - } - - if (error == null) { - // include task ids? - listener.onResponse(new AcknowledgedResponse(true)); - } else { - // TODO: cancel all started tasks - listener.onFailure(error); - } - } - } - }); - } - }, listener::onFailure)); - } - } - -} diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestCcrStatsAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestCcrStatsAction.java index df34fd6cd45..0cf0aaf2e49 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestCcrStatsAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestCcrStatsAction.java @@ -14,7 +14,7 @@ import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.action.RestToXContentListener; -import org.elasticsearch.xpack.ccr.action.CcrStatsAction; +import org.elasticsearch.xpack.core.ccr.action.CcrStatsAction; import java.io.IOException; diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestCreateAndFollowIndexAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestCreateAndFollowIndexAction.java index 4d9079b36c9..8816760f526 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestCreateAndFollowIndexAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestCreateAndFollowIndexAction.java @@ -14,8 +14,8 @@ import org.elasticsearch.rest.action.RestToXContentListener; import java.io.IOException; -import static org.elasticsearch.xpack.ccr.action.CreateAndFollowIndexAction.INSTANCE; -import static org.elasticsearch.xpack.ccr.action.CreateAndFollowIndexAction.Request; +import static org.elasticsearch.xpack.core.ccr.action.CreateAndFollowIndexAction.INSTANCE; +import static org.elasticsearch.xpack.core.ccr.action.CreateAndFollowIndexAction.Request; public class RestCreateAndFollowIndexAction extends BaseRestHandler { diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestFollowIndexAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestFollowIndexAction.java index 88f5b74f4b1..8a1d7d778bd 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestFollowIndexAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestFollowIndexAction.java @@ -15,8 +15,8 @@ import org.elasticsearch.rest.action.RestToXContentListener; import java.io.IOException; -import static org.elasticsearch.xpack.ccr.action.FollowIndexAction.INSTANCE; -import static org.elasticsearch.xpack.ccr.action.FollowIndexAction.Request; +import static org.elasticsearch.xpack.core.ccr.action.FollowIndexAction.INSTANCE; +import static org.elasticsearch.xpack.core.ccr.action.FollowIndexAction.Request; public class RestFollowIndexAction extends BaseRestHandler { diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestUnfollowIndexAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestUnfollowIndexAction.java index 2df6c77379b..9a82717b621 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestUnfollowIndexAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestUnfollowIndexAction.java @@ -14,8 +14,8 @@ import org.elasticsearch.rest.action.RestToXContentListener; import java.io.IOException; -import static org.elasticsearch.xpack.ccr.action.UnfollowIndexAction.INSTANCE; -import static org.elasticsearch.xpack.ccr.action.UnfollowIndexAction.Request; +import static org.elasticsearch.xpack.core.ccr.action.UnfollowIndexAction.INSTANCE; +import static org.elasticsearch.xpack.core.ccr.action.UnfollowIndexAction.Request; public class RestUnfollowIndexAction extends BaseRestHandler { diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/CcrLicenseIT.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/CcrLicenseIT.java index 2d58358d11f..ecf2bd47fc7 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/CcrLicenseIT.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/CcrLicenseIT.java @@ -22,11 +22,10 @@ import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESSingleNodeTestCase; import org.elasticsearch.test.MockLogAppender; import org.elasticsearch.xpack.ccr.action.AutoFollowCoordinator; -import org.elasticsearch.xpack.ccr.action.CcrStatsAction; -import org.elasticsearch.xpack.ccr.action.CreateAndFollowIndexAction; -import org.elasticsearch.xpack.ccr.action.FollowIndexAction; +import org.elasticsearch.xpack.core.ccr.action.CcrStatsAction; +import org.elasticsearch.xpack.core.ccr.action.CreateAndFollowIndexAction; +import org.elasticsearch.xpack.core.ccr.action.FollowIndexAction; import org.elasticsearch.xpack.ccr.action.PutAutoFollowPatternAction; -import org.elasticsearch.xpack.ccr.action.ShardFollowNodeTask; import org.elasticsearch.xpack.core.ccr.AutoFollowMetadata; import org.elasticsearch.xpack.core.ccr.AutoFollowMetadata.AutoFollowPattern; @@ -196,11 +195,11 @@ public class CcrLicenseIT extends ESSingleNodeTestCase { return new FollowIndexAction.Request( "leader", "follower", - ShardFollowNodeTask.DEFAULT_MAX_BATCH_OPERATION_COUNT, - ShardFollowNodeTask.DEFAULT_MAX_CONCURRENT_READ_BATCHES, - ShardFollowNodeTask.DEFAULT_MAX_BATCH_SIZE_IN_BYTES, - ShardFollowNodeTask.DEFAULT_MAX_CONCURRENT_WRITE_BATCHES, - ShardFollowNodeTask.DEFAULT_MAX_WRITE_BUFFER_SIZE, + FollowIndexAction.DEFAULT_MAX_BATCH_OPERATION_COUNT, + FollowIndexAction.DEFAULT_MAX_CONCURRENT_READ_BATCHES, + FollowIndexAction.DEFAULT_MAX_BATCH_SIZE_IN_BYTES, + FollowIndexAction.DEFAULT_MAX_CONCURRENT_WRITE_BATCHES, + FollowIndexAction.DEFAULT_MAX_WRITE_BUFFER_SIZE, TimeValue.timeValueMillis(10), TimeValue.timeValueMillis(10)); } diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/ShardChangesIT.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/ShardChangesIT.java index 7980e128140..c0919f25fe3 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/ShardChangesIT.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/ShardChangesIT.java @@ -38,13 +38,13 @@ import org.elasticsearch.test.ESIntegTestCase; import org.elasticsearch.test.MockHttpTransport; import org.elasticsearch.test.discovery.TestZenDiscovery; import org.elasticsearch.test.junit.annotations.TestLogging; -import org.elasticsearch.xpack.ccr.action.CreateAndFollowIndexAction; -import org.elasticsearch.xpack.ccr.action.FollowIndexAction; +import org.elasticsearch.xpack.core.ccr.action.CreateAndFollowIndexAction; +import org.elasticsearch.xpack.core.ccr.action.FollowIndexAction; import org.elasticsearch.xpack.ccr.action.ShardChangesAction; -import org.elasticsearch.xpack.ccr.action.ShardFollowNodeTask; import org.elasticsearch.xpack.ccr.action.ShardFollowTask; -import org.elasticsearch.xpack.ccr.action.UnfollowIndexAction; +import org.elasticsearch.xpack.core.ccr.action.UnfollowIndexAction; import org.elasticsearch.xpack.core.XPackSettings; +import org.elasticsearch.xpack.core.ccr.ShardFollowNodeTaskStatus; import java.io.IOException; import java.util.Arrays; @@ -335,7 +335,7 @@ public class ShardChangesIT extends ESIntegTestCase { final FollowIndexAction.Request followRequest = new FollowIndexAction.Request("index1", "index2", randomIntBetween(32, 2048), randomIntBetween(2, 10), Long.MAX_VALUE, randomIntBetween(2, 10), - ShardFollowNodeTask.DEFAULT_MAX_WRITE_BUFFER_SIZE, TimeValue.timeValueMillis(500), TimeValue.timeValueMillis(10)); + FollowIndexAction.DEFAULT_MAX_WRITE_BUFFER_SIZE, TimeValue.timeValueMillis(500), TimeValue.timeValueMillis(10)); client().execute(FollowIndexAction.INSTANCE, followRequest).get(); long maxNumDocsReplicated = Math.min(1000, randomLongBetween(followRequest.getMaxBatchOperationCount(), @@ -507,7 +507,7 @@ public class ShardChangesIT extends ESIntegTestCase { } } assertThat(taskInfo, notNullValue()); - ShardFollowNodeTask.Status status = (ShardFollowNodeTask.Status) taskInfo.getStatus(); + ShardFollowNodeTaskStatus status = (ShardFollowNodeTaskStatus) taskInfo.getStatus(); assertThat(status, notNullValue()); assertThat("incorrect global checkpoint " + shardFollowTaskParams, status.followerGlobalCheckpoint(), @@ -665,9 +665,9 @@ public class ShardChangesIT extends ESIntegTestCase { } public static FollowIndexAction.Request createFollowRequest(String leaderIndex, String followIndex) { - return new FollowIndexAction.Request(leaderIndex, followIndex, ShardFollowNodeTask.DEFAULT_MAX_BATCH_OPERATION_COUNT, - ShardFollowNodeTask.DEFAULT_MAX_CONCURRENT_READ_BATCHES, ShardFollowNodeTask.DEFAULT_MAX_BATCH_SIZE_IN_BYTES, - ShardFollowNodeTask.DEFAULT_MAX_CONCURRENT_WRITE_BATCHES, ShardFollowNodeTask.DEFAULT_MAX_WRITE_BUFFER_SIZE, + return new FollowIndexAction.Request(leaderIndex, followIndex, FollowIndexAction.DEFAULT_MAX_BATCH_OPERATION_COUNT, + FollowIndexAction.DEFAULT_MAX_CONCURRENT_READ_BATCHES, FollowIndexAction.DEFAULT_MAX_BATCH_SIZE_IN_BYTES, + FollowIndexAction.DEFAULT_MAX_CONCURRENT_WRITE_BATCHES, FollowIndexAction.DEFAULT_MAX_WRITE_BUFFER_SIZE, TimeValue.timeValueMillis(10), TimeValue.timeValueMillis(10)); } } diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinatorTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinatorTests.java index 2ef84129232..5ab11cf5b0c 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinatorTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinatorTests.java @@ -17,6 +17,7 @@ import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.ccr.action.AutoFollowCoordinator.AutoFollower; import org.elasticsearch.xpack.core.ccr.AutoFollowMetadata; import org.elasticsearch.xpack.core.ccr.AutoFollowMetadata.AutoFollowPattern; +import org.elasticsearch.xpack.core.ccr.action.FollowIndexAction; import java.util.ArrayList; import java.util.Collections; diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/CreateAndFollowIndexRequestTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/CreateAndFollowIndexRequestTests.java index c68d1849965..c751ca5f000 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/CreateAndFollowIndexRequestTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/CreateAndFollowIndexRequestTests.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.ccr.action; import org.elasticsearch.test.AbstractStreamableTestCase; +import org.elasticsearch.xpack.core.ccr.action.CreateAndFollowIndexAction; public class CreateAndFollowIndexRequestTests extends AbstractStreamableTestCase { diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/CreateAndFollowIndexResponseTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/CreateAndFollowIndexResponseTests.java index 11a518ef067..44ac21055a7 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/CreateAndFollowIndexResponseTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/CreateAndFollowIndexResponseTests.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.ccr.action; import org.elasticsearch.test.AbstractStreamableTestCase; +import org.elasticsearch.xpack.core.ccr.action.CreateAndFollowIndexAction; public class CreateAndFollowIndexResponseTests extends AbstractStreamableTestCase { diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/FollowIndexRequestTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/FollowIndexRequestTests.java index 7202f7202c6..2017fa2fdb9 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/FollowIndexRequestTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/FollowIndexRequestTests.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.ccr.action; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractStreamableXContentTestCase; +import org.elasticsearch.xpack.core.ccr.action.FollowIndexAction; import java.io.IOException; diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskRandomTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskRandomTests.java index 9bfd6b9d6ef..dacb60372e6 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskRandomTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskRandomTests.java @@ -15,6 +15,8 @@ import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.ccr.action.bulk.BulkShardOperationsResponse; +import org.elasticsearch.xpack.core.ccr.action.FollowIndexAction; +import org.elasticsearch.xpack.core.ccr.ShardFollowNodeTaskStatus; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -52,7 +54,7 @@ public class ShardFollowNodeTaskRandomTests extends ESTestCase { private void startAndAssertAndStopTask(ShardFollowNodeTask task, TestRun testRun) throws Exception { task.start(testRun.startSeqNo - 1, testRun.startSeqNo - 1, testRun.startSeqNo - 1, testRun.startSeqNo - 1); assertBusy(() -> { - ShardFollowNodeTask.Status status = task.getStatus(); + ShardFollowNodeTaskStatus status = task.getStatus(); assertThat(status.leaderGlobalCheckpoint(), equalTo(testRun.finalExpectedGlobalCheckpoint)); assertThat(status.followerGlobalCheckpoint(), equalTo(testRun.finalExpectedGlobalCheckpoint)); final long numberOfFailedFetches = @@ -65,7 +67,7 @@ public class ShardFollowNodeTaskRandomTests extends ESTestCase { task.markAsCompleted(); assertBusy(() -> { - ShardFollowNodeTask.Status status = task.getStatus(); + ShardFollowNodeTaskStatus status = task.getStatus(); assertThat(status.numberOfConcurrentReads(), equalTo(0)); assertThat(status.numberOfConcurrentWrites(), equalTo(0)); }); @@ -75,7 +77,7 @@ public class ShardFollowNodeTaskRandomTests extends ESTestCase { AtomicBoolean stopped = new AtomicBoolean(false); ShardFollowTask params = new ShardFollowTask(null, new ShardId("follow_index", "", 0), new ShardId("leader_index", "", 0), testRun.maxOperationCount, concurrency, - ShardFollowNodeTask.DEFAULT_MAX_BATCH_SIZE_IN_BYTES, concurrency, 10240, + FollowIndexAction.DEFAULT_MAX_BATCH_SIZE_IN_BYTES, concurrency, 10240, TimeValue.timeValueMillis(10), TimeValue.timeValueMillis(10), Collections.emptyMap()); ThreadPool threadPool = new TestThreadPool(getClass().getSimpleName()); diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskStatusTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskStatusTests.java index 8368a818e00..2f145e7a98c 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskStatusTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskStatusTests.java @@ -10,6 +10,7 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractSerializingTestCase; +import org.elasticsearch.xpack.core.ccr.ShardFollowNodeTaskStatus; import java.io.IOException; import java.util.Map; @@ -21,17 +22,17 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; -public class ShardFollowNodeTaskStatusTests extends AbstractSerializingTestCase { +public class ShardFollowNodeTaskStatusTests extends AbstractSerializingTestCase { @Override - protected ShardFollowNodeTask.Status doParseInstance(XContentParser parser) throws IOException { - return ShardFollowNodeTask.Status.fromXContent(parser); + protected ShardFollowNodeTaskStatus doParseInstance(XContentParser parser) throws IOException { + return ShardFollowNodeTaskStatus.fromXContent(parser); } @Override - protected ShardFollowNodeTask.Status createTestInstance() { + protected ShardFollowNodeTaskStatus createTestInstance() { // if you change this constructor, reflect the changes in the hand-written assertions below - return new ShardFollowNodeTask.Status( + return new ShardFollowNodeTaskStatus( randomAlphaOfLength(4), randomInt(), randomNonNegativeLong(), @@ -57,7 +58,7 @@ public class ShardFollowNodeTaskStatusTests extends AbstractSerializingTestCase< } @Override - protected void assertEqualInstances(final ShardFollowNodeTask.Status expectedInstance, final ShardFollowNodeTask.Status newInstance) { + protected void assertEqualInstances(final ShardFollowNodeTaskStatus expectedInstance, final ShardFollowNodeTaskStatus newInstance) { assertNotSame(expectedInstance, newInstance); assertThat(newInstance.leaderIndex(), equalTo(expectedInstance.leaderIndex())); assertThat(newInstance.getShardId(), equalTo(expectedInstance.getShardId())); @@ -108,8 +109,8 @@ public class ShardFollowNodeTaskStatusTests extends AbstractSerializingTestCase< } @Override - protected Writeable.Reader instanceReader() { - return ShardFollowNodeTask.Status::new; + protected Writeable.Reader instanceReader() { + return ShardFollowNodeTaskStatus::new; } } diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskTests.java index 4f7c0bf1664..e177f77e613 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskTests.java @@ -13,6 +13,7 @@ import org.elasticsearch.index.shard.ShardNotFoundException; import org.elasticsearch.index.translog.Translog; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.ccr.action.bulk.BulkShardOperationsResponse; +import org.elasticsearch.xpack.core.ccr.ShardFollowNodeTaskStatus; import java.net.ConnectException; import java.nio.charset.StandardCharsets; @@ -44,7 +45,7 @@ public class ShardFollowNodeTaskTests extends ESTestCase { private List> bulkShardOperationRequests; private BiConsumer scheduler = (delay, task) -> task.run(); - private Consumer beforeSendShardChangesRequest = status -> {}; + private Consumer beforeSendShardChangesRequest = status -> {}; private AtomicBoolean simulateResponse = new AtomicBoolean(); @@ -66,7 +67,7 @@ public class ShardFollowNodeTaskTests extends ESTestCase { assertThat(shardChangesRequests, contains(new long[][]{ {6L, 8L}, {14L, 8L}, {22L, 8L}, {30L, 8L}, {38L, 8L}, {46L, 8L}, {54L, 7L}} )); - ShardFollowNodeTask.Status status = task.getStatus(); + ShardFollowNodeTaskStatus status = task.getStatus(); assertThat(status.numberOfConcurrentReads(), equalTo(7)); assertThat(status.lastRequestedSeqNo(), equalTo(60L)); } @@ -86,7 +87,7 @@ public class ShardFollowNodeTaskTests extends ESTestCase { task.innerHandleReadResponse(0L, 63L, generateShardChangesResponse(0, 63, 0L, 128L)); assertThat(shardChangesRequests.size(), equalTo(0)); // no more reads, because write buffer is full - ShardFollowNodeTask.Status status = task.getStatus(); + ShardFollowNodeTaskStatus status = task.getStatus(); assertThat(status.numberOfConcurrentReads(), equalTo(0)); assertThat(status.numberOfConcurrentWrites(), equalTo(0)); assertThat(status.lastRequestedSeqNo(), equalTo(63L)); @@ -102,7 +103,7 @@ public class ShardFollowNodeTaskTests extends ESTestCase { assertThat(shardChangesRequests.get(0)[0], equalTo(0L)); assertThat(shardChangesRequests.get(0)[1], equalTo(8L)); - ShardFollowNodeTask.Status status = task.getStatus(); + ShardFollowNodeTaskStatus status = task.getStatus(); assertThat(status.numberOfConcurrentReads(), equalTo(1)); assertThat(status.lastRequestedSeqNo(), equalTo(7L)); } @@ -140,7 +141,7 @@ public class ShardFollowNodeTaskTests extends ESTestCase { assertThat(shardChangesRequests.size(), equalTo(0)); // no more reads, because task has been cancelled assertThat(bulkShardOperationRequests.size(), equalTo(0)); // no more writes, because task has been cancelled - ShardFollowNodeTask.Status status = task.getStatus(); + ShardFollowNodeTaskStatus status = task.getStatus(); assertThat(status.numberOfConcurrentReads(), equalTo(0)); assertThat(status.numberOfConcurrentWrites(), equalTo(0)); assertThat(status.lastRequestedSeqNo(), equalTo(15L)); @@ -164,7 +165,7 @@ public class ShardFollowNodeTaskTests extends ESTestCase { assertThat(shardChangesRequests.size(), equalTo(0)); // no more reads, because task has been cancelled assertThat(bulkShardOperationRequests.size(), equalTo(0)); // no more writes, because task has been cancelled - ShardFollowNodeTask.Status status = task.getStatus(); + ShardFollowNodeTaskStatus status = task.getStatus(); assertThat(status.numberOfConcurrentReads(), equalTo(0)); assertThat(status.numberOfConcurrentWrites(), equalTo(0)); assertThat(status.lastRequestedSeqNo(), equalTo(63L)); @@ -211,7 +212,7 @@ public class ShardFollowNodeTaskTests extends ESTestCase { } assertFalse("task is not stopped", task.isStopped()); - ShardFollowNodeTask.Status status = task.getStatus(); + ShardFollowNodeTaskStatus status = task.getStatus(); assertThat(status.numberOfConcurrentReads(), equalTo(1)); assertThat(status.numberOfConcurrentWrites(), equalTo(0)); assertThat(status.numberOfFailedFetches(), equalTo((long)max)); @@ -258,7 +259,7 @@ public class ShardFollowNodeTaskTests extends ESTestCase { assertTrue("task is stopped", task.isStopped()); assertThat(fatalError, notNullValue()); assertThat(fatalError.getMessage(), containsString("retrying failed [")); - ShardFollowNodeTask.Status status = task.getStatus(); + ShardFollowNodeTaskStatus status = task.getStatus(); assertThat(status.numberOfConcurrentReads(), equalTo(1)); assertThat(status.numberOfConcurrentWrites(), equalTo(0)); assertThat(status.numberOfFailedFetches(), equalTo(11L)); @@ -299,7 +300,7 @@ public class ShardFollowNodeTaskTests extends ESTestCase { assertTrue("task is stopped", task.isStopped()); assertThat(fatalError, sameInstance(failure)); - ShardFollowNodeTask.Status status = task.getStatus(); + ShardFollowNodeTaskStatus status = task.getStatus(); assertThat(status.numberOfConcurrentReads(), equalTo(1)); assertThat(status.numberOfConcurrentWrites(), equalTo(0)); assertThat(status.numberOfFailedFetches(), equalTo(1L)); @@ -326,7 +327,7 @@ public class ShardFollowNodeTaskTests extends ESTestCase { assertThat(bulkShardOperationRequests.size(), equalTo(1)); assertThat(bulkShardOperationRequests.get(0), equalTo(Arrays.asList(response.getOperations()))); - ShardFollowNodeTask.Status status = task.getStatus(); + ShardFollowNodeTaskStatus status = task.getStatus(); assertThat(status.mappingVersion(), equalTo(0L)); assertThat(status.numberOfConcurrentReads(), equalTo(1)); assertThat(status.numberOfConcurrentReads(), equalTo(1)); @@ -353,7 +354,7 @@ public class ShardFollowNodeTaskTests extends ESTestCase { assertThat(shardChangesRequests.get(0)[0], equalTo(21L)); assertThat(shardChangesRequests.get(0)[1], equalTo(43L)); - ShardFollowNodeTask.Status status = task.getStatus(); + ShardFollowNodeTaskStatus status = task.getStatus(); assertThat(status.numberOfConcurrentReads(), equalTo(1)); assertThat(status.numberOfConcurrentWrites(), equalTo(1)); assertThat(status.lastRequestedSeqNo(), equalTo(63L)); @@ -376,7 +377,7 @@ public class ShardFollowNodeTaskTests extends ESTestCase { assertThat(shardChangesRequests.size(), equalTo(0)); assertThat(bulkShardOperationRequests.size(), equalTo(0)); - ShardFollowNodeTask.Status status = task.getStatus(); + ShardFollowNodeTaskStatus status = task.getStatus(); assertThat(status.numberOfConcurrentReads(), equalTo(0)); assertThat(status.numberOfConcurrentWrites(), equalTo(0)); assertThat(status.lastRequestedSeqNo(), equalTo(63L)); @@ -399,7 +400,7 @@ public class ShardFollowNodeTaskTests extends ESTestCase { assertThat(shardChangesRequests.get(0)[0], equalTo(0L)); assertThat(shardChangesRequests.get(0)[1], equalTo(64L)); - ShardFollowNodeTask.Status status = task.getStatus(); + ShardFollowNodeTaskStatus status = task.getStatus(); assertThat(status.numberOfConcurrentReads(), equalTo(1)); assertThat(status.numberOfConcurrentWrites(), equalTo(0)); assertThat(status.lastRequestedSeqNo(), equalTo(63L)); @@ -441,7 +442,7 @@ public class ShardFollowNodeTaskTests extends ESTestCase { assertThat(bulkShardOperationRequests.size(), equalTo(1)); assertThat(bulkShardOperationRequests.get(0), equalTo(Arrays.asList(response.getOperations()))); - ShardFollowNodeTask.Status status = task.getStatus(); + ShardFollowNodeTaskStatus status = task.getStatus(); assertThat(status.mappingVersion(), equalTo(1L)); assertThat(status.numberOfConcurrentReads(), equalTo(1)); assertThat(status.numberOfConcurrentWrites(), equalTo(1)); @@ -466,7 +467,7 @@ public class ShardFollowNodeTaskTests extends ESTestCase { assertThat(mappingUpdateFailures.size(), equalTo(0)); assertThat(bulkShardOperationRequests.size(), equalTo(1)); assertThat(task.isStopped(), equalTo(false)); - ShardFollowNodeTask.Status status = task.getStatus(); + ShardFollowNodeTaskStatus status = task.getStatus(); assertThat(status.mappingVersion(), equalTo(1L)); assertThat(status.numberOfConcurrentReads(), equalTo(1)); assertThat(status.numberOfConcurrentWrites(), equalTo(1)); @@ -492,7 +493,7 @@ public class ShardFollowNodeTaskTests extends ESTestCase { assertThat(mappingVersions.size(), equalTo(1)); assertThat(bulkShardOperationRequests.size(), equalTo(0)); assertThat(task.isStopped(), equalTo(true)); - ShardFollowNodeTask.Status status = task.getStatus(); + ShardFollowNodeTaskStatus status = task.getStatus(); assertThat(status.mappingVersion(), equalTo(0L)); assertThat(status.numberOfConcurrentReads(), equalTo(1)); assertThat(status.numberOfConcurrentWrites(), equalTo(0)); @@ -511,7 +512,7 @@ public class ShardFollowNodeTaskTests extends ESTestCase { assertThat(bulkShardOperationRequests.size(), equalTo(0)); assertThat(task.isStopped(), equalTo(true)); - ShardFollowNodeTask.Status status = task.getStatus(); + ShardFollowNodeTaskStatus status = task.getStatus(); assertThat(status.mappingVersion(), equalTo(0L)); assertThat(status.numberOfConcurrentReads(), equalTo(1)); assertThat(status.numberOfConcurrentWrites(), equalTo(0)); @@ -535,7 +536,7 @@ public class ShardFollowNodeTaskTests extends ESTestCase { assertThat(bulkShardOperationRequests.size(), equalTo(1)); assertThat(bulkShardOperationRequests.get(0), equalTo(Arrays.asList(response.getOperations()))); - ShardFollowNodeTask.Status status = task.getStatus(); + ShardFollowNodeTaskStatus status = task.getStatus(); assertThat(status.numberOfConcurrentReads(), equalTo(1)); assertThat(status.numberOfConcurrentWrites(), equalTo(1)); assertThat(status.lastRequestedSeqNo(), equalTo(63L)); @@ -553,7 +554,7 @@ public class ShardFollowNodeTaskTests extends ESTestCase { assertThat(bulkShardOperationRequests.get(0), equalTo(Arrays.asList(response.getOperations()).subList(0, 64))); assertThat(bulkShardOperationRequests.get(1), equalTo(Arrays.asList(response.getOperations()).subList(64, 128))); - ShardFollowNodeTask.Status status = task.getStatus(); + ShardFollowNodeTaskStatus status = task.getStatus(); assertThat(status.numberOfConcurrentWrites(), equalTo(2)); task = createShardFollowTask(64, 1, 4, Integer.MAX_VALUE, Long.MAX_VALUE); @@ -583,7 +584,7 @@ public class ShardFollowNodeTaskTests extends ESTestCase { assertThat(bulkShardOperationRequests.get(i), equalTo(Arrays.asList(response.getOperations()).subList(offset, offset + 8))); } - ShardFollowNodeTask.Status status = task.getStatus(); + ShardFollowNodeTaskStatus status = task.getStatus(); assertThat(status.numberOfConcurrentWrites(), equalTo(32)); } @@ -610,7 +611,7 @@ public class ShardFollowNodeTaskTests extends ESTestCase { assertThat(operations, equalTo(Arrays.asList(response.getOperations()))); } assertThat(task.isStopped(), equalTo(false)); - ShardFollowNodeTask.Status status = task.getStatus(); + ShardFollowNodeTaskStatus status = task.getStatus(); assertThat(status.numberOfConcurrentWrites(), equalTo(1)); assertThat(status.followerGlobalCheckpoint(), equalTo(-1L)); } @@ -638,7 +639,7 @@ public class ShardFollowNodeTaskTests extends ESTestCase { assertThat(operations, equalTo(Arrays.asList(response.getOperations()))); } assertThat(task.isStopped(), equalTo(true)); - ShardFollowNodeTask.Status status = task.getStatus(); + ShardFollowNodeTaskStatus status = task.getStatus(); assertThat(status.numberOfConcurrentWrites(), equalTo(1)); assertThat(status.followerGlobalCheckpoint(), equalTo(-1L)); } @@ -660,7 +661,7 @@ public class ShardFollowNodeTaskTests extends ESTestCase { assertThat(bulkShardOperationRequests.size(), equalTo(1)); assertThat(bulkShardOperationRequests.get(0), equalTo(Arrays.asList(response.getOperations()))); assertThat(task.isStopped(), equalTo(true)); - ShardFollowNodeTask.Status status = task.getStatus(); + ShardFollowNodeTaskStatus status = task.getStatus(); assertThat(status.numberOfConcurrentWrites(), equalTo(1)); assertThat(status.followerGlobalCheckpoint(), equalTo(-1L)); } @@ -704,7 +705,7 @@ public class ShardFollowNodeTaskTests extends ESTestCase { assertThat(shardChangesRequests.get(0)[0], equalTo(64L)); assertThat(shardChangesRequests.get(0)[1], equalTo(64L)); - ShardFollowNodeTask.Status status = task.getStatus(); + ShardFollowNodeTaskStatus status = task.getStatus(); assertThat(status.numberOfConcurrentReads(), equalTo(1)); assertThat(status.lastRequestedSeqNo(), equalTo(63L)); assertThat(status.leaderGlobalCheckpoint(), equalTo(63L)); diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/FollowIndexActionTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexActionTests.java similarity index 89% rename from x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/FollowIndexActionTests.java rename to x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexActionTests.java index 5b52700f557..7691945643d 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/FollowIndexActionTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexActionTests.java @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + package org.elasticsearch.xpack.ccr.action; import org.elasticsearch.Version; @@ -15,32 +16,33 @@ import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.ccr.CcrSettings; import org.elasticsearch.xpack.ccr.ShardChangesIT; +import org.elasticsearch.xpack.core.ccr.action.FollowIndexAction; import java.io.IOException; +import static org.elasticsearch.xpack.ccr.action.TransportFollowIndexAction.validate; import static org.hamcrest.Matchers.equalTo; -public class FollowIndexActionTests extends ESTestCase { +public class TransportFollowIndexActionTests extends ESTestCase { public void testValidation() throws IOException { FollowIndexAction.Request request = ShardChangesIT.createFollowRequest("index1", "index2"); { // should fail, because leader index does not exist - Exception e = expectThrows(IllegalArgumentException.class, () -> FollowIndexAction.validate(request, null, null, null)); + Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, null, null, null)); assertThat(e.getMessage(), equalTo("leader index [index1] does not exist")); } { // should fail, because follow index does not exist IndexMetaData leaderIMD = createIMD("index1", 5, Settings.EMPTY); - Exception e = expectThrows(IllegalArgumentException.class, () -> FollowIndexAction.validate(request, leaderIMD, null, null)); + Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, null, null)); assertThat(e.getMessage(), equalTo("follow index [index2] does not exist")); } { // should fail because leader index does not have soft deletes enabled IndexMetaData leaderIMD = createIMD("index1", 5, Settings.EMPTY); IndexMetaData followIMD = createIMD("index2", 5, Settings.EMPTY); - Exception e = expectThrows(IllegalArgumentException.class, - () -> FollowIndexAction.validate(request, leaderIMD, followIMD, null)); + Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, null)); assertThat(e.getMessage(), equalTo("leader index [index1] does not have soft deletes enabled")); } { @@ -48,8 +50,7 @@ public class FollowIndexActionTests extends ESTestCase { IndexMetaData leaderIMD = createIMD("index1", 5, Settings.builder() .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build()); IndexMetaData followIMD = createIMD("index2", 4, Settings.EMPTY); - Exception e = expectThrows(IllegalArgumentException.class, - () -> FollowIndexAction.validate(request, leaderIMD, followIMD, null)); + Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, null)); assertThat(e.getMessage(), equalTo("leader index primary shards [5] does not match with the number of shards of the follow index [4]")); } @@ -59,8 +60,7 @@ public class FollowIndexActionTests extends ESTestCase { .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build()); IndexMetaData followIMD = createIMD("index2", State.OPEN, "{}", 5, Settings.builder() .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build()); - Exception e = expectThrows(IllegalArgumentException.class, - () -> FollowIndexAction.validate(request, leaderIMD, followIMD, null)); + Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, null)); assertThat(e.getMessage(), equalTo("leader and follow index must be open")); } { @@ -71,8 +71,7 @@ public class FollowIndexActionTests extends ESTestCase { Settings.builder().put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true).build()); MapperService mapperService = MapperTestUtils.newMapperService(xContentRegistry(), createTempDir(), Settings.EMPTY, "index2"); mapperService.updateMapping(null, followIMD); - Exception e = expectThrows(IllegalArgumentException.class, - () -> FollowIndexAction.validate(request, leaderIMD, followIMD, mapperService)); + Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, mapperService)); assertThat(e.getMessage(), equalTo("mapper [field] of different type, current_type [text], merged_type [keyword]")); } { @@ -86,8 +85,7 @@ public class FollowIndexActionTests extends ESTestCase { .put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true) .put("index.analysis.analyzer.my_analyzer.type", "custom") .put("index.analysis.analyzer.my_analyzer.tokenizer", "standard").build()); - Exception e = expectThrows(IllegalArgumentException.class, - () -> FollowIndexAction.validate(request, leaderIMD, followIMD, null)); + Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, null)); assertThat(e.getMessage(), equalTo("the leader and follower index settings must be identical")); } { @@ -100,8 +98,8 @@ public class FollowIndexActionTests extends ESTestCase { MapperService mapperService = MapperTestUtils.newMapperService(xContentRegistry(), createTempDir(), followingIndexSettings, "index2"); mapperService.updateMapping(null, followIMD); - IllegalArgumentException error = expectThrows(IllegalArgumentException.class, - () -> FollowIndexAction.validate(request, leaderIMD, followIMD, mapperService)); + IllegalArgumentException error = + expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, mapperService)); assertThat(error.getMessage(), equalTo("the following index [index2] is not ready to follow; " + "the setting [index.xpack.ccr.following_index] must be enabled.")); } @@ -113,7 +111,7 @@ public class FollowIndexActionTests extends ESTestCase { .put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true).build()); MapperService mapperService = MapperTestUtils.newMapperService(xContentRegistry(), createTempDir(), Settings.EMPTY, "index2"); mapperService.updateMapping(null, followIMD); - FollowIndexAction.validate(request, leaderIMD, followIMD, mapperService); + validate(request, leaderIMD, followIMD, mapperService); } { // should succeed, index settings are identical @@ -129,7 +127,7 @@ public class FollowIndexActionTests extends ESTestCase { MapperService mapperService = MapperTestUtils.newMapperService(xContentRegistry(), createTempDir(), followIMD.getSettings(), "index2"); mapperService.updateMapping(null, followIMD); - FollowIndexAction.validate(request, leaderIMD, followIMD, mapperService); + validate(request, leaderIMD, followIMD, mapperService); } { // should succeed despite whitelisted settings being different @@ -147,7 +145,7 @@ public class FollowIndexActionTests extends ESTestCase { MapperService mapperService = MapperTestUtils.newMapperService(xContentRegistry(), createTempDir(), followIMD.getSettings(), "index2"); mapperService.updateMapping(null, followIMD); - FollowIndexAction.validate(request, leaderIMD, followIMD, mapperService); + validate(request, leaderIMD, followIMD, mapperService); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClient.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClient.java index 77f511ba4d0..3f27f66b27b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClient.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClient.java @@ -13,6 +13,7 @@ import org.elasticsearch.protocol.xpack.XPackInfoRequest; import org.elasticsearch.protocol.xpack.XPackInfoResponse; import org.elasticsearch.xpack.core.action.XPackInfoAction; import org.elasticsearch.xpack.core.action.XPackInfoRequestBuilder; +import org.elasticsearch.xpack.core.ccr.client.CcrClient; import org.elasticsearch.xpack.core.ml.client.MachineLearningClient; import org.elasticsearch.xpack.core.monitoring.client.MonitoringClient; import org.elasticsearch.xpack.core.security.client.SecurityClient; @@ -20,6 +21,7 @@ import org.elasticsearch.xpack.core.watcher.client.WatcherClient; import java.util.Collections; import java.util.Map; +import java.util.Objects; import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.BASIC_AUTH_HEADER; import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; @@ -28,6 +30,7 @@ public class XPackClient { private final Client client; + private final CcrClient ccrClient; private final LicensingClient licensingClient; private final MonitoringClient monitoringClient; private final SecurityClient securityClient; @@ -35,7 +38,8 @@ public class XPackClient { private final MachineLearningClient machineLearning; public XPackClient(Client client) { - this.client = client; + this.client = Objects.requireNonNull(client, "client"); + this.ccrClient = new CcrClient(client); this.licensingClient = new LicensingClient(client); this.monitoringClient = new MonitoringClient(client); this.securityClient = new SecurityClient(client); @@ -47,6 +51,10 @@ public class XPackClient { return client; } + public CcrClient ccr() { + return ccrClient; + } + public LicensingClient licensing() { return licensingClient; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/ShardFollowNodeTaskStatus.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/ShardFollowNodeTaskStatus.java new file mode 100644 index 00000000000..783999cf183 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/ShardFollowNodeTaskStatus.java @@ -0,0 +1,504 @@ +/* + * 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.ccr; + +import org.elasticsearch.ElasticsearchException; +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.unit.ByteSizeUnit; +import org.elasticsearch.common.unit.ByteSizeValue; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.tasks.Task; + +import java.io.IOException; +import java.util.AbstractMap; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.Objects; +import java.util.TreeMap; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +public class ShardFollowNodeTaskStatus implements Task.Status { + + public static final String STATUS_PARSER_NAME = "shard-follow-node-task-status"; + + private static final ParseField LEADER_INDEX = new ParseField("leader_index"); + private static final ParseField SHARD_ID = new ParseField("shard_id"); + private static final ParseField LEADER_GLOBAL_CHECKPOINT_FIELD = new ParseField("leader_global_checkpoint"); + private static final ParseField LEADER_MAX_SEQ_NO_FIELD = new ParseField("leader_max_seq_no"); + private static final ParseField FOLLOWER_GLOBAL_CHECKPOINT_FIELD = new ParseField("follower_global_checkpoint"); + private static final ParseField FOLLOWER_MAX_SEQ_NO_FIELD = new ParseField("follower_max_seq_no"); + private static final ParseField LAST_REQUESTED_SEQ_NO_FIELD = new ParseField("last_requested_seq_no"); + private static final ParseField NUMBER_OF_CONCURRENT_READS_FIELD = new ParseField("number_of_concurrent_reads"); + private static final ParseField NUMBER_OF_CONCURRENT_WRITES_FIELD = new ParseField("number_of_concurrent_writes"); + private static final ParseField NUMBER_OF_QUEUED_WRITES_FIELD = new ParseField("number_of_queued_writes"); + private static final ParseField MAPPING_VERSION_FIELD = new ParseField("mapping_version"); + private static final ParseField TOTAL_FETCH_TIME_MILLIS_FIELD = new ParseField("total_fetch_time_millis"); + private static final ParseField NUMBER_OF_SUCCESSFUL_FETCHES_FIELD = new ParseField("number_of_successful_fetches"); + private static final ParseField NUMBER_OF_FAILED_FETCHES_FIELD = new ParseField("number_of_failed_fetches"); + private static final ParseField OPERATIONS_RECEIVED_FIELD = new ParseField("operations_received"); + private static final ParseField TOTAL_TRANSFERRED_BYTES = new ParseField("total_transferred_bytes"); + private static final ParseField TOTAL_INDEX_TIME_MILLIS_FIELD = new ParseField("total_index_time_millis"); + private static final ParseField NUMBER_OF_SUCCESSFUL_BULK_OPERATIONS_FIELD = new ParseField("number_of_successful_bulk_operations"); + private static final ParseField NUMBER_OF_FAILED_BULK_OPERATIONS_FIELD = new ParseField("number_of_failed_bulk_operations"); + private static final ParseField NUMBER_OF_OPERATIONS_INDEXED_FIELD = new ParseField("number_of_operations_indexed"); + private static final ParseField FETCH_EXCEPTIONS = new ParseField("fetch_exceptions"); + private static final ParseField TIME_SINCE_LAST_FETCH_MILLIS_FIELD = new ParseField("time_since_last_fetch_millis"); + + @SuppressWarnings("unchecked") + static final ConstructingObjectParser STATUS_PARSER = + new ConstructingObjectParser<>( + STATUS_PARSER_NAME, + args -> new ShardFollowNodeTaskStatus( + (String) args[0], + (int) args[1], + (long) args[2], + (long) args[3], + (long) args[4], + (long) args[5], + (long) args[6], + (int) args[7], + (int) args[8], + (int) args[9], + (long) args[10], + (long) args[11], + (long) args[12], + (long) args[13], + (long) args[14], + (long) args[15], + (long) args[16], + (long) args[17], + (long) args[18], + (long) args[19], + new TreeMap<>( + ((List>) args[20]) + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))), + (long) args[21])); + + public static final String FETCH_EXCEPTIONS_ENTRY_PARSER_NAME = "shard-follow-node-task-status-fetch-exceptions-entry"; + + static final ConstructingObjectParser, Void> FETCH_EXCEPTIONS_ENTRY_PARSER = + new ConstructingObjectParser<>( + FETCH_EXCEPTIONS_ENTRY_PARSER_NAME, + args -> new AbstractMap.SimpleEntry<>((long) args[0], (ElasticsearchException) args[1])); + + static { + STATUS_PARSER.declareString(ConstructingObjectParser.constructorArg(), LEADER_INDEX); + STATUS_PARSER.declareInt(ConstructingObjectParser.constructorArg(), SHARD_ID); + STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), LEADER_GLOBAL_CHECKPOINT_FIELD); + STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), LEADER_MAX_SEQ_NO_FIELD); + STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), FOLLOWER_GLOBAL_CHECKPOINT_FIELD); + STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), FOLLOWER_MAX_SEQ_NO_FIELD); + STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), LAST_REQUESTED_SEQ_NO_FIELD); + STATUS_PARSER.declareInt(ConstructingObjectParser.constructorArg(), NUMBER_OF_CONCURRENT_READS_FIELD); + STATUS_PARSER.declareInt(ConstructingObjectParser.constructorArg(), NUMBER_OF_CONCURRENT_WRITES_FIELD); + STATUS_PARSER.declareInt(ConstructingObjectParser.constructorArg(), NUMBER_OF_QUEUED_WRITES_FIELD); + STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), MAPPING_VERSION_FIELD); + STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), TOTAL_FETCH_TIME_MILLIS_FIELD); + STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), NUMBER_OF_SUCCESSFUL_FETCHES_FIELD); + STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), NUMBER_OF_FAILED_FETCHES_FIELD); + STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), OPERATIONS_RECEIVED_FIELD); + STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), TOTAL_TRANSFERRED_BYTES); + STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), TOTAL_INDEX_TIME_MILLIS_FIELD); + STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), NUMBER_OF_SUCCESSFUL_BULK_OPERATIONS_FIELD); + STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), NUMBER_OF_FAILED_BULK_OPERATIONS_FIELD); + STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), NUMBER_OF_OPERATIONS_INDEXED_FIELD); + STATUS_PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(), FETCH_EXCEPTIONS_ENTRY_PARSER, FETCH_EXCEPTIONS); + STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), TIME_SINCE_LAST_FETCH_MILLIS_FIELD); + } + + static final ParseField FETCH_EXCEPTIONS_ENTRY_FROM_SEQ_NO = new ParseField("from_seq_no"); + static final ParseField FETCH_EXCEPTIONS_ENTRY_EXCEPTION = new ParseField("exception"); + + static { + FETCH_EXCEPTIONS_ENTRY_PARSER.declareLong(ConstructingObjectParser.constructorArg(), FETCH_EXCEPTIONS_ENTRY_FROM_SEQ_NO); + FETCH_EXCEPTIONS_ENTRY_PARSER.declareObject( + ConstructingObjectParser.constructorArg(), + (p, c) -> ElasticsearchException.fromXContent(p), + FETCH_EXCEPTIONS_ENTRY_EXCEPTION); + } + + private final String leaderIndex; + + public String leaderIndex() { + return leaderIndex; + } + + private final int shardId; + + public int getShardId() { + return shardId; + } + + private final long leaderGlobalCheckpoint; + + public long leaderGlobalCheckpoint() { + return leaderGlobalCheckpoint; + } + + private final long leaderMaxSeqNo; + + public long leaderMaxSeqNo() { + return leaderMaxSeqNo; + } + + private final long followerGlobalCheckpoint; + + public long followerGlobalCheckpoint() { + return followerGlobalCheckpoint; + } + + private final long followerMaxSeqNo; + + public long followerMaxSeqNo() { + return followerMaxSeqNo; + } + + private final long lastRequestedSeqNo; + + public long lastRequestedSeqNo() { + return lastRequestedSeqNo; + } + + private final int numberOfConcurrentReads; + + public int numberOfConcurrentReads() { + return numberOfConcurrentReads; + } + + private final int numberOfConcurrentWrites; + + public int numberOfConcurrentWrites() { + return numberOfConcurrentWrites; + } + + private final int numberOfQueuedWrites; + + public int numberOfQueuedWrites() { + return numberOfQueuedWrites; + } + + private final long mappingVersion; + + public long mappingVersion() { + return mappingVersion; + } + + private final long totalFetchTimeMillis; + + public long totalFetchTimeMillis() { + return totalFetchTimeMillis; + } + + private final long numberOfSuccessfulFetches; + + public long numberOfSuccessfulFetches() { + return numberOfSuccessfulFetches; + } + + private final long numberOfFailedFetches; + + public long numberOfFailedFetches() { + return numberOfFailedFetches; + } + + private final long operationsReceived; + + public long operationsReceived() { + return operationsReceived; + } + + private final long totalTransferredBytes; + + public long totalTransferredBytes() { + return totalTransferredBytes; + } + + private final long totalIndexTimeMillis; + + public long totalIndexTimeMillis() { + return totalIndexTimeMillis; + } + + private final long numberOfSuccessfulBulkOperations; + + public long numberOfSuccessfulBulkOperations() { + return numberOfSuccessfulBulkOperations; + } + + private final long numberOfFailedBulkOperations; + + public long numberOfFailedBulkOperations() { + return numberOfFailedBulkOperations; + } + + private final long numberOfOperationsIndexed; + + public long numberOfOperationsIndexed() { + return numberOfOperationsIndexed; + } + + private final NavigableMap fetchExceptions; + + public NavigableMap fetchExceptions() { + return fetchExceptions; + } + + private final long timeSinceLastFetchMillis; + + public long timeSinceLastFetchMillis() { + return timeSinceLastFetchMillis; + } + + public ShardFollowNodeTaskStatus( + final String leaderIndex, + final int shardId, + final long leaderGlobalCheckpoint, + final long leaderMaxSeqNo, + final long followerGlobalCheckpoint, + final long followerMaxSeqNo, + final long lastRequestedSeqNo, + final int numberOfConcurrentReads, + final int numberOfConcurrentWrites, + final int numberOfQueuedWrites, + final long mappingVersion, + final long totalFetchTimeMillis, + final long numberOfSuccessfulFetches, + final long numberOfFailedFetches, + final long operationsReceived, + final long totalTransferredBytes, + final long totalIndexTimeMillis, + final long numberOfSuccessfulBulkOperations, + final long numberOfFailedBulkOperations, + final long numberOfOperationsIndexed, + final NavigableMap fetchExceptions, + final long timeSinceLastFetchMillis) { + this.leaderIndex = leaderIndex; + this.shardId = shardId; + this.leaderGlobalCheckpoint = leaderGlobalCheckpoint; + this.leaderMaxSeqNo = leaderMaxSeqNo; + this.followerGlobalCheckpoint = followerGlobalCheckpoint; + this.followerMaxSeqNo = followerMaxSeqNo; + this.lastRequestedSeqNo = lastRequestedSeqNo; + this.numberOfConcurrentReads = numberOfConcurrentReads; + this.numberOfConcurrentWrites = numberOfConcurrentWrites; + this.numberOfQueuedWrites = numberOfQueuedWrites; + this.mappingVersion = mappingVersion; + this.totalFetchTimeMillis = totalFetchTimeMillis; + this.numberOfSuccessfulFetches = numberOfSuccessfulFetches; + this.numberOfFailedFetches = numberOfFailedFetches; + this.operationsReceived = operationsReceived; + this.totalTransferredBytes = totalTransferredBytes; + this.totalIndexTimeMillis = totalIndexTimeMillis; + this.numberOfSuccessfulBulkOperations = numberOfSuccessfulBulkOperations; + this.numberOfFailedBulkOperations = numberOfFailedBulkOperations; + this.numberOfOperationsIndexed = numberOfOperationsIndexed; + this.fetchExceptions = Objects.requireNonNull(fetchExceptions); + this.timeSinceLastFetchMillis = timeSinceLastFetchMillis; + } + + public ShardFollowNodeTaskStatus(final StreamInput in) throws IOException { + this.leaderIndex = in.readString(); + this.shardId = in.readVInt(); + this.leaderGlobalCheckpoint = in.readZLong(); + this.leaderMaxSeqNo = in.readZLong(); + this.followerGlobalCheckpoint = in.readZLong(); + this.followerMaxSeqNo = in.readZLong(); + this.lastRequestedSeqNo = in.readZLong(); + this.numberOfConcurrentReads = in.readVInt(); + this.numberOfConcurrentWrites = in.readVInt(); + this.numberOfQueuedWrites = in.readVInt(); + this.mappingVersion = in.readVLong(); + this.totalFetchTimeMillis = in.readVLong(); + this.numberOfSuccessfulFetches = in.readVLong(); + this.numberOfFailedFetches = in.readVLong(); + this.operationsReceived = in.readVLong(); + this.totalTransferredBytes = in.readVLong(); + this.totalIndexTimeMillis = in.readVLong(); + this.numberOfSuccessfulBulkOperations = in.readVLong(); + this.numberOfFailedBulkOperations = in.readVLong(); + this.numberOfOperationsIndexed = in.readVLong(); + this.fetchExceptions = new TreeMap<>(in.readMap(StreamInput::readVLong, StreamInput::readException)); + this.timeSinceLastFetchMillis = in.readZLong(); + } + + @Override + public String getWriteableName() { + return STATUS_PARSER_NAME; + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + out.writeString(leaderIndex); + out.writeVInt(shardId); + out.writeZLong(leaderGlobalCheckpoint); + out.writeZLong(leaderMaxSeqNo); + out.writeZLong(followerGlobalCheckpoint); + out.writeZLong(followerMaxSeqNo); + out.writeZLong(lastRequestedSeqNo); + out.writeVInt(numberOfConcurrentReads); + out.writeVInt(numberOfConcurrentWrites); + out.writeVInt(numberOfQueuedWrites); + out.writeVLong(mappingVersion); + out.writeVLong(totalFetchTimeMillis); + out.writeVLong(numberOfSuccessfulFetches); + out.writeVLong(numberOfFailedFetches); + out.writeVLong(operationsReceived); + out.writeVLong(totalTransferredBytes); + out.writeVLong(totalIndexTimeMillis); + out.writeVLong(numberOfSuccessfulBulkOperations); + out.writeVLong(numberOfFailedBulkOperations); + out.writeVLong(numberOfOperationsIndexed); + out.writeMap(fetchExceptions, StreamOutput::writeVLong, StreamOutput::writeException); + out.writeZLong(timeSinceLastFetchMillis); + } + + @Override + public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException { + builder.startObject(); + { + builder.field(LEADER_INDEX.getPreferredName(), leaderIndex); + builder.field(SHARD_ID.getPreferredName(), shardId); + builder.field(LEADER_GLOBAL_CHECKPOINT_FIELD.getPreferredName(), leaderGlobalCheckpoint); + builder.field(LEADER_MAX_SEQ_NO_FIELD.getPreferredName(), leaderMaxSeqNo); + builder.field(FOLLOWER_GLOBAL_CHECKPOINT_FIELD.getPreferredName(), followerGlobalCheckpoint); + builder.field(FOLLOWER_MAX_SEQ_NO_FIELD.getPreferredName(), followerMaxSeqNo); + builder.field(LAST_REQUESTED_SEQ_NO_FIELD.getPreferredName(), lastRequestedSeqNo); + builder.field(NUMBER_OF_CONCURRENT_READS_FIELD.getPreferredName(), numberOfConcurrentReads); + builder.field(NUMBER_OF_CONCURRENT_WRITES_FIELD.getPreferredName(), numberOfConcurrentWrites); + builder.field(NUMBER_OF_QUEUED_WRITES_FIELD.getPreferredName(), numberOfQueuedWrites); + builder.field(MAPPING_VERSION_FIELD.getPreferredName(), mappingVersion); + builder.humanReadableField( + TOTAL_FETCH_TIME_MILLIS_FIELD.getPreferredName(), + "total_fetch_time", + new TimeValue(totalFetchTimeMillis, TimeUnit.MILLISECONDS)); + builder.field(NUMBER_OF_SUCCESSFUL_FETCHES_FIELD.getPreferredName(), numberOfSuccessfulFetches); + builder.field(NUMBER_OF_FAILED_FETCHES_FIELD.getPreferredName(), numberOfFailedFetches); + builder.field(OPERATIONS_RECEIVED_FIELD.getPreferredName(), operationsReceived); + builder.humanReadableField( + TOTAL_TRANSFERRED_BYTES.getPreferredName(), + "total_transferred", + new ByteSizeValue(totalTransferredBytes, ByteSizeUnit.BYTES)); + builder.humanReadableField( + TOTAL_INDEX_TIME_MILLIS_FIELD.getPreferredName(), + "total_index_time", + new TimeValue(totalIndexTimeMillis, TimeUnit.MILLISECONDS)); + builder.field(NUMBER_OF_SUCCESSFUL_BULK_OPERATIONS_FIELD.getPreferredName(), numberOfSuccessfulBulkOperations); + builder.field(NUMBER_OF_FAILED_BULK_OPERATIONS_FIELD.getPreferredName(), numberOfFailedBulkOperations); + builder.field(NUMBER_OF_OPERATIONS_INDEXED_FIELD.getPreferredName(), numberOfOperationsIndexed); + builder.startArray(FETCH_EXCEPTIONS.getPreferredName()); + { + for (final Map.Entry entry : fetchExceptions.entrySet()) { + builder.startObject(); + { + builder.field(FETCH_EXCEPTIONS_ENTRY_FROM_SEQ_NO.getPreferredName(), entry.getKey()); + builder.field(FETCH_EXCEPTIONS_ENTRY_EXCEPTION.getPreferredName()); + builder.startObject(); + { + ElasticsearchException.generateThrowableXContent(builder, params, entry.getValue()); + } + builder.endObject(); + } + builder.endObject(); + } + } + builder.endArray(); + builder.humanReadableField( + TIME_SINCE_LAST_FETCH_MILLIS_FIELD.getPreferredName(), + "time_since_last_fetch", + new TimeValue(timeSinceLastFetchMillis, TimeUnit.MILLISECONDS)); + } + builder.endObject(); + return builder; + } + + public static ShardFollowNodeTaskStatus fromXContent(final XContentParser parser) { + return STATUS_PARSER.apply(parser, null); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final ShardFollowNodeTaskStatus that = (ShardFollowNodeTaskStatus) o; + return leaderIndex.equals(that.leaderIndex) && + shardId == that.shardId && + leaderGlobalCheckpoint == that.leaderGlobalCheckpoint && + leaderMaxSeqNo == that.leaderMaxSeqNo && + followerGlobalCheckpoint == that.followerGlobalCheckpoint && + followerMaxSeqNo == that.followerMaxSeqNo && + lastRequestedSeqNo == that.lastRequestedSeqNo && + numberOfConcurrentReads == that.numberOfConcurrentReads && + numberOfConcurrentWrites == that.numberOfConcurrentWrites && + numberOfQueuedWrites == that.numberOfQueuedWrites && + mappingVersion == that.mappingVersion && + totalFetchTimeMillis == that.totalFetchTimeMillis && + numberOfSuccessfulFetches == that.numberOfSuccessfulFetches && + numberOfFailedFetches == that.numberOfFailedFetches && + operationsReceived == that.operationsReceived && + totalTransferredBytes == that.totalTransferredBytes && + numberOfSuccessfulBulkOperations == that.numberOfSuccessfulBulkOperations && + numberOfFailedBulkOperations == that.numberOfFailedBulkOperations && + numberOfOperationsIndexed == that.numberOfOperationsIndexed && + /* + * ElasticsearchException does not implement equals so we will assume the fetch exceptions are equal if they are equal + * up to the key set and their messages. Note that we are relying on the fact that the fetch exceptions are ordered by + * keys. + */ + fetchExceptions.keySet().equals(that.fetchExceptions.keySet()) && + getFetchExceptionMessages(this).equals(getFetchExceptionMessages(that)) && + timeSinceLastFetchMillis == that.timeSinceLastFetchMillis; + } + + @Override + public int hashCode() { + return Objects.hash( + leaderIndex, + shardId, + leaderGlobalCheckpoint, + leaderMaxSeqNo, + followerGlobalCheckpoint, + followerMaxSeqNo, + lastRequestedSeqNo, + numberOfConcurrentReads, + numberOfConcurrentWrites, + numberOfQueuedWrites, + mappingVersion, + totalFetchTimeMillis, + numberOfSuccessfulFetches, + numberOfFailedFetches, + operationsReceived, + totalTransferredBytes, + numberOfSuccessfulBulkOperations, + numberOfFailedBulkOperations, + numberOfOperationsIndexed, + /* + * ElasticsearchException does not implement hash code so we will compute the hash code based on the key set and the + * messages. Note that we are relying on the fact that the fetch exceptions are ordered by keys. + */ + fetchExceptions.keySet(), + getFetchExceptionMessages(this), + timeSinceLastFetchMillis); + } + + private static List getFetchExceptionMessages(final ShardFollowNodeTaskStatus status) { + return status.fetchExceptions().values().stream().map(ElasticsearchException::getMessage).collect(Collectors.toList()); + } + + public String toString() { + return Strings.toString(this); + } + +} diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/CcrStatsAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/CcrStatsAction.java similarity index 92% rename from x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/CcrStatsAction.java rename to x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/CcrStatsAction.java index b5d6697fc73..ace3d6bb194 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/CcrStatsAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/CcrStatsAction.java @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -package org.elasticsearch.xpack.ccr.action; +package org.elasticsearch.xpack.core.ccr.action; import org.elasticsearch.action.Action; import org.elasticsearch.action.ActionRequestValidationException; @@ -21,6 +21,7 @@ import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.tasks.Task; +import org.elasticsearch.xpack.core.ccr.ShardFollowNodeTaskStatus; import java.io.IOException; import java.util.Collections; @@ -51,7 +52,7 @@ public class CcrStatsAction extends Action { this(Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); } - TasksResponse( + public TasksResponse( final List taskFailures, final List nodeFailures, final List taskResponses) { @@ -151,20 +152,20 @@ public class CcrStatsAction extends Action { return followerShardId; } - private final ShardFollowNodeTask.Status status; + private final ShardFollowNodeTaskStatus status; - ShardFollowNodeTask.Status status() { + ShardFollowNodeTaskStatus status() { return status; } - TaskResponse(final ShardId followerShardId, final ShardFollowNodeTask.Status status) { + public TaskResponse(final ShardId followerShardId, final ShardFollowNodeTaskStatus status) { this.followerShardId = followerShardId; this.status = status; } - TaskResponse(final StreamInput in) throws IOException { + public TaskResponse(final StreamInput in) throws IOException { this.followerShardId = ShardId.readShardId(in); - this.status = new ShardFollowNodeTask.Status(in); + this.status = new ShardFollowNodeTaskStatus(in); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/CreateAndFollowIndexAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/CreateAndFollowIndexAction.java new file mode 100644 index 00000000000..ea63815c2b9 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/CreateAndFollowIndexAction.java @@ -0,0 +1,167 @@ +/* + * 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.ccr.action; + +import org.elasticsearch.action.Action; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.IndicesRequest; +import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.action.support.master.AcknowledgedRequest; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Objects; + +public final class CreateAndFollowIndexAction extends Action { + + public static final CreateAndFollowIndexAction INSTANCE = new CreateAndFollowIndexAction(); + public static final String NAME = "indices:admin/xpack/ccr/create_and_follow_index"; + + private CreateAndFollowIndexAction() { + super(NAME); + } + + @Override + public Response newResponse() { + return new Response(); + } + + public static class Request extends AcknowledgedRequest implements IndicesRequest { + + private FollowIndexAction.Request followRequest; + + public Request(FollowIndexAction.Request followRequest) { + this.followRequest = Objects.requireNonNull(followRequest); + } + + public Request() { + + } + + public FollowIndexAction.Request getFollowRequest() { + return followRequest; + } + + @Override + public ActionRequestValidationException validate() { + return followRequest.validate(); + } + + @Override + public String[] indices() { + return new String[]{followRequest.getFollowerIndex()}; + } + + @Override + public IndicesOptions indicesOptions() { + return IndicesOptions.strictSingleIndexNoExpandForbidClosed(); + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + followRequest = new FollowIndexAction.Request(); + followRequest.readFrom(in); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + followRequest.writeTo(out); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Request request = (Request) o; + return Objects.equals(followRequest, request.followRequest); + } + + @Override + public int hashCode() { + return Objects.hash(followRequest); + } + } + + public static class Response extends ActionResponse implements ToXContentObject { + + private boolean followIndexCreated; + private boolean followIndexShardsAcked; + private boolean indexFollowingStarted; + + public Response() { + + } + + public Response(boolean followIndexCreated, boolean followIndexShardsAcked, boolean indexFollowingStarted) { + this.followIndexCreated = followIndexCreated; + this.followIndexShardsAcked = followIndexShardsAcked; + this.indexFollowingStarted = indexFollowingStarted; + } + + public boolean isFollowIndexCreated() { + return followIndexCreated; + } + + public boolean isFollowIndexShardsAcked() { + return followIndexShardsAcked; + } + + public boolean isIndexFollowingStarted() { + return indexFollowingStarted; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + followIndexCreated = in.readBoolean(); + followIndexShardsAcked = in.readBoolean(); + indexFollowingStarted = in.readBoolean(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeBoolean(followIndexCreated); + out.writeBoolean(followIndexShardsAcked); + out.writeBoolean(indexFollowingStarted); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + { + builder.field("follow_index_created", followIndexCreated); + builder.field("follow_index_shards_acked", followIndexShardsAcked); + builder.field("index_following_started", indexFollowingStarted); + } + builder.endObject(); + return builder; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Response response = (Response) o; + return followIndexCreated == response.followIndexCreated && + followIndexShardsAcked == response.followIndexShardsAcked && + indexFollowingStarted == response.indexFollowingStarted; + } + + @Override + public int hashCode() { + return Objects.hash(followIndexCreated, followIndexShardsAcked, indexFollowingStarted); + } + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/FollowIndexAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/FollowIndexAction.java new file mode 100644 index 00000000000..c42ef8db9c1 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/FollowIndexAction.java @@ -0,0 +1,307 @@ +/* + * 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.ccr.action; + +import org.elasticsearch.action.Action; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Objects; + +public final class FollowIndexAction extends Action { + + public static final FollowIndexAction INSTANCE = new FollowIndexAction(); + public static final String NAME = "cluster:admin/xpack/ccr/follow_index"; + + public static final int DEFAULT_MAX_WRITE_BUFFER_SIZE = 10240; + public static final int DEFAULT_MAX_BATCH_OPERATION_COUNT = 1024; + public static final int DEFAULT_MAX_CONCURRENT_READ_BATCHES = 1; + public static final int DEFAULT_MAX_CONCURRENT_WRITE_BATCHES = 1; + public static final long DEFAULT_MAX_BATCH_SIZE_IN_BYTES = Long.MAX_VALUE; + public static final int RETRY_LIMIT = 10; + public static final TimeValue DEFAULT_RETRY_TIMEOUT = new TimeValue(500); + public static final TimeValue DEFAULT_IDLE_SHARD_RETRY_DELAY = TimeValue.timeValueSeconds(10); + + private FollowIndexAction() { + super(NAME); + } + + @Override + public AcknowledgedResponse newResponse() { + return new AcknowledgedResponse(); + } + + public static class Request extends ActionRequest implements ToXContentObject { + + private static final ParseField LEADER_INDEX_FIELD = new ParseField("leader_index"); + private static final ParseField FOLLOWER_INDEX_FIELD = new ParseField("follower_index"); + private static final ParseField MAX_BATCH_OPERATION_COUNT = new ParseField("max_batch_operation_count"); + private static final ParseField MAX_CONCURRENT_READ_BATCHES = new ParseField("max_concurrent_read_batches"); + private static final ParseField MAX_BATCH_SIZE_IN_BYTES = new ParseField("max_batch_size_in_bytes"); + private static final ParseField MAX_CONCURRENT_WRITE_BATCHES = new ParseField("max_concurrent_write_batches"); + private static final ParseField MAX_WRITE_BUFFER_SIZE = new ParseField("max_write_buffer_size"); + private static final ParseField RETRY_TIMEOUT = new ParseField("retry_timeout"); + private static final ParseField IDLE_SHARD_RETRY_DELAY = new ParseField("idle_shard_retry_delay"); + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, true, + (args, followerIndex) -> { + if (args[1] != null) { + followerIndex = (String) args[1]; + } + return new Request((String) args[0], followerIndex, (Integer) args[2], (Integer) args[3], (Long) args[4], + (Integer) args[5], (Integer) args[6], (TimeValue) args[7], (TimeValue) args[8]); + }); + + static { + PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), LEADER_INDEX_FIELD); + PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), FOLLOWER_INDEX_FIELD); + PARSER.declareInt(ConstructingObjectParser.optionalConstructorArg(), MAX_BATCH_OPERATION_COUNT); + PARSER.declareInt(ConstructingObjectParser.optionalConstructorArg(), MAX_CONCURRENT_READ_BATCHES); + PARSER.declareLong(ConstructingObjectParser.optionalConstructorArg(), MAX_BATCH_SIZE_IN_BYTES); + PARSER.declareInt(ConstructingObjectParser.optionalConstructorArg(), MAX_CONCURRENT_WRITE_BATCHES); + PARSER.declareInt(ConstructingObjectParser.optionalConstructorArg(), MAX_WRITE_BUFFER_SIZE); + PARSER.declareField( + ConstructingObjectParser.optionalConstructorArg(), + (p, c) -> TimeValue.parseTimeValue(p.text(), RETRY_TIMEOUT.getPreferredName()), + RETRY_TIMEOUT, + ObjectParser.ValueType.STRING); + PARSER.declareField( + ConstructingObjectParser.optionalConstructorArg(), + (p, c) -> TimeValue.parseTimeValue(p.text(), IDLE_SHARD_RETRY_DELAY.getPreferredName()), + IDLE_SHARD_RETRY_DELAY, + ObjectParser.ValueType.STRING); + } + + public static Request fromXContent(final XContentParser parser, final String followerIndex) throws IOException { + Request request = PARSER.parse(parser, followerIndex); + if (followerIndex != null) { + if (request.followerIndex == null) { + request.followerIndex = followerIndex; + } else { + if (request.followerIndex.equals(followerIndex) == false) { + throw new IllegalArgumentException("provided follower_index is not equal"); + } + } + } + return request; + } + + private String leaderIndex; + + public String getLeaderIndex() { + return leaderIndex; + } + + + private String followerIndex; + + public String getFollowerIndex() { + return followerIndex; + } + + private int maxBatchOperationCount; + + public int getMaxBatchOperationCount() { + return maxBatchOperationCount; + } + + private int maxConcurrentReadBatches; + + public int getMaxConcurrentReadBatches() { + return maxConcurrentReadBatches; + } + + private long maxOperationSizeInBytes; + + public long getMaxOperationSizeInBytes() { + return maxOperationSizeInBytes; + } + + private int maxConcurrentWriteBatches; + + public int getMaxConcurrentWriteBatches() { + return maxConcurrentWriteBatches; + } + + private int maxWriteBufferSize; + + public int getMaxWriteBufferSize() { + return maxWriteBufferSize; + } + + private TimeValue retryTimeout; + + public TimeValue getRetryTimeout() { + return retryTimeout; + } + + private TimeValue idleShardRetryDelay; + + public TimeValue getIdleShardRetryDelay() { + return idleShardRetryDelay; + } + + public Request( + final String leaderIndex, + final String followerIndex, + final Integer maxBatchOperationCount, + final Integer maxConcurrentReadBatches, + final Long maxOperationSizeInBytes, + final Integer maxConcurrentWriteBatches, + final Integer maxWriteBufferSize, + final TimeValue retryTimeout, + final TimeValue idleShardRetryDelay) { + + if (leaderIndex == null) { + throw new IllegalArgumentException(LEADER_INDEX_FIELD.getPreferredName() + " is missing"); + } + + if (followerIndex == null) { + throw new IllegalArgumentException(FOLLOWER_INDEX_FIELD.getPreferredName() + " is missing"); + } + + final int actualMaxBatchOperationCount = + maxBatchOperationCount == null ? DEFAULT_MAX_BATCH_OPERATION_COUNT : maxBatchOperationCount; + if (actualMaxBatchOperationCount < 1) { + throw new IllegalArgumentException(MAX_BATCH_OPERATION_COUNT.getPreferredName() + " must be larger than 0"); + } + + final int actualMaxConcurrentReadBatches = + maxConcurrentReadBatches == null ? DEFAULT_MAX_CONCURRENT_READ_BATCHES : maxConcurrentReadBatches; + if (actualMaxConcurrentReadBatches < 1) { + throw new IllegalArgumentException(MAX_CONCURRENT_READ_BATCHES.getPreferredName() + " must be larger than 0"); + } + + final long actualMaxOperationSizeInBytes = + maxOperationSizeInBytes == null ? DEFAULT_MAX_BATCH_SIZE_IN_BYTES : maxOperationSizeInBytes; + if (actualMaxOperationSizeInBytes <= 0) { + throw new IllegalArgumentException(MAX_BATCH_SIZE_IN_BYTES.getPreferredName() + " must be larger than 0"); + } + + final int actualMaxConcurrentWriteBatches = + maxConcurrentWriteBatches == null ? DEFAULT_MAX_CONCURRENT_WRITE_BATCHES : maxConcurrentWriteBatches; + if (actualMaxConcurrentWriteBatches < 1) { + throw new IllegalArgumentException(MAX_CONCURRENT_WRITE_BATCHES.getPreferredName() + " must be larger than 0"); + } + + final int actualMaxWriteBufferSize = maxWriteBufferSize == null ? DEFAULT_MAX_WRITE_BUFFER_SIZE : maxWriteBufferSize; + if (actualMaxWriteBufferSize < 1) { + throw new IllegalArgumentException(MAX_WRITE_BUFFER_SIZE.getPreferredName() + " must be larger than 0"); + } + + final TimeValue actualRetryTimeout = retryTimeout == null ? DEFAULT_RETRY_TIMEOUT : retryTimeout; + final TimeValue actualIdleShardRetryDelay = idleShardRetryDelay == null ? DEFAULT_IDLE_SHARD_RETRY_DELAY : idleShardRetryDelay; + + this.leaderIndex = leaderIndex; + this.followerIndex = followerIndex; + this.maxBatchOperationCount = actualMaxBatchOperationCount; + this.maxConcurrentReadBatches = actualMaxConcurrentReadBatches; + this.maxOperationSizeInBytes = actualMaxOperationSizeInBytes; + this.maxConcurrentWriteBatches = actualMaxConcurrentWriteBatches; + this.maxWriteBufferSize = actualMaxWriteBufferSize; + this.retryTimeout = actualRetryTimeout; + this.idleShardRetryDelay = actualIdleShardRetryDelay; + } + + public Request() { + + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + @Override + public void readFrom(final StreamInput in) throws IOException { + super.readFrom(in); + leaderIndex = in.readString(); + followerIndex = in.readString(); + maxBatchOperationCount = in.readVInt(); + maxConcurrentReadBatches = in.readVInt(); + maxOperationSizeInBytes = in.readVLong(); + maxConcurrentWriteBatches = in.readVInt(); + maxWriteBufferSize = in.readVInt(); + retryTimeout = in.readOptionalTimeValue(); + idleShardRetryDelay = in.readOptionalTimeValue(); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(leaderIndex); + out.writeString(followerIndex); + out.writeVInt(maxBatchOperationCount); + out.writeVInt(maxConcurrentReadBatches); + out.writeVLong(maxOperationSizeInBytes); + out.writeVInt(maxConcurrentWriteBatches); + out.writeVInt(maxWriteBufferSize); + out.writeOptionalTimeValue(retryTimeout); + out.writeOptionalTimeValue(idleShardRetryDelay); + } + + @Override + public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException { + builder.startObject(); + { + builder.field(LEADER_INDEX_FIELD.getPreferredName(), leaderIndex); + builder.field(FOLLOWER_INDEX_FIELD.getPreferredName(), followerIndex); + builder.field(MAX_BATCH_OPERATION_COUNT.getPreferredName(), maxBatchOperationCount); + builder.field(MAX_BATCH_SIZE_IN_BYTES.getPreferredName(), maxOperationSizeInBytes); + builder.field(MAX_WRITE_BUFFER_SIZE.getPreferredName(), maxWriteBufferSize); + builder.field(MAX_CONCURRENT_READ_BATCHES.getPreferredName(), maxConcurrentReadBatches); + builder.field(MAX_CONCURRENT_WRITE_BATCHES.getPreferredName(), maxConcurrentWriteBatches); + builder.field(RETRY_TIMEOUT.getPreferredName(), retryTimeout.getStringRep()); + builder.field(IDLE_SHARD_RETRY_DELAY.getPreferredName(), idleShardRetryDelay.getStringRep()); + } + builder.endObject(); + return builder; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Request request = (Request) o; + return maxBatchOperationCount == request.maxBatchOperationCount && + maxConcurrentReadBatches == request.maxConcurrentReadBatches && + maxOperationSizeInBytes == request.maxOperationSizeInBytes && + maxConcurrentWriteBatches == request.maxConcurrentWriteBatches && + maxWriteBufferSize == request.maxWriteBufferSize && + Objects.equals(retryTimeout, request.retryTimeout) && + Objects.equals(idleShardRetryDelay, request.idleShardRetryDelay) && + Objects.equals(leaderIndex, request.leaderIndex) && + Objects.equals(followerIndex, request.followerIndex); + } + + @Override + public int hashCode() { + return Objects.hash( + leaderIndex, + followerIndex, + maxBatchOperationCount, + maxConcurrentReadBatches, + maxOperationSizeInBytes, + maxConcurrentWriteBatches, + maxWriteBufferSize, + retryTimeout, + idleShardRetryDelay + ); + } + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/UnfollowIndexAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/UnfollowIndexAction.java new file mode 100644 index 00000000000..65ecd3dad2f --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/UnfollowIndexAction.java @@ -0,0 +1,62 @@ +/* + * 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.ccr.action; + +import org.elasticsearch.action.Action; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; + +public class UnfollowIndexAction extends Action { + + public static final UnfollowIndexAction INSTANCE = new UnfollowIndexAction(); + public static final String NAME = "cluster:admin/xpack/ccr/unfollow_index"; + + private UnfollowIndexAction() { + super(NAME); + } + + @Override + public AcknowledgedResponse newResponse() { + return new AcknowledgedResponse(); + } + + public static class Request extends ActionRequest { + + private String followIndex; + + public String getFollowIndex() { + return followIndex; + } + + public void setFollowIndex(final String followIndex) { + this.followIndex = followIndex; + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + @Override + public void readFrom(final StreamInput in) throws IOException { + super.readFrom(in); + followIndex = in.readString(); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(followIndex); + } + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/client/CcrClient.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/client/CcrClient.java new file mode 100644 index 00000000000..e880b384824 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/client/CcrClient.java @@ -0,0 +1,73 @@ +/* + * 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.ccr.client; + +import org.elasticsearch.action.ActionFuture; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.client.ElasticsearchClient; +import org.elasticsearch.xpack.core.ccr.action.CcrStatsAction; +import org.elasticsearch.xpack.core.ccr.action.CreateAndFollowIndexAction; +import org.elasticsearch.xpack.core.ccr.action.FollowIndexAction; +import org.elasticsearch.xpack.core.ccr.action.UnfollowIndexAction; + +import java.util.Objects; + +public class CcrClient { + + private final ElasticsearchClient client; + + public CcrClient(final ElasticsearchClient client) { + this.client = Objects.requireNonNull(client, "client"); + } + + public void createAndFollow( + final CreateAndFollowIndexAction.Request request, + final ActionListener listener) { + client.execute(CreateAndFollowIndexAction.INSTANCE, request, listener); + } + + public ActionFuture createAndFollow(final CreateAndFollowIndexAction.Request request) { + final PlainActionFuture listener = PlainActionFuture.newFuture(); + client.execute(CreateAndFollowIndexAction.INSTANCE, request, listener); + return listener; + } + + public void follow(final FollowIndexAction.Request request, final ActionListener listener) { + client.execute(FollowIndexAction.INSTANCE, request, listener); + } + + public ActionFuture follow(final FollowIndexAction.Request request) { + final PlainActionFuture listener = PlainActionFuture.newFuture(); + client.execute(FollowIndexAction.INSTANCE, request, listener); + return listener; + } + + public void stats( + final CcrStatsAction.TasksRequest request, + final ActionListener listener) { + client.execute(CcrStatsAction.INSTANCE, request, listener); + } + + public ActionFuture stats(final CcrStatsAction.TasksRequest request) { + final PlainActionFuture listener = PlainActionFuture.newFuture(); + client.execute(CcrStatsAction.INSTANCE, request, listener); + return listener; + } + + public void unfollow(final UnfollowIndexAction.Request request, final ActionListener listener) { + client.execute(UnfollowIndexAction.INSTANCE, request, listener); + } + + public ActionFuture unfollow(final UnfollowIndexAction.Request request) { + final PlainActionFuture listener = PlainActionFuture.newFuture(); + client.execute(UnfollowIndexAction.INSTANCE, request, listener); + return listener; + } + +} From 9f8dff9281e5ff16c9e819944125174a0160d17b Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Tue, 11 Sep 2018 19:11:30 -0400 Subject: [PATCH 15/78] Remove debug logging in full cluster restart tests (#33612) These logs are incredibly verbose, and it makes chasing normal failures burdensome. This commit removes the debug logging, which can be reenabled again if needed. --- qa/full-cluster-restart/build.gradle | 6 ------ x-pack/qa/full-cluster-restart/build.gradle | 6 ------ 2 files changed, 12 deletions(-) diff --git a/qa/full-cluster-restart/build.gradle b/qa/full-cluster-restart/build.gradle index ca8371e30e7..8b305462e4d 100644 --- a/qa/full-cluster-restart/build.gradle +++ b/qa/full-cluster-restart/build.gradle @@ -53,9 +53,6 @@ for (Version version : bwcVersions.indexCompatible) { // some tests rely on the translog not being flushed setting 'indices.memory.shard_inactive_time', '20m' - // debug logging for testRecovery - setting 'logger.level', 'DEBUG' - if (version.onOrAfter('5.3.0')) { setting 'http.content_type.required', 'true' } @@ -75,9 +72,6 @@ for (Version version : bwcVersions.indexCompatible) { // some tests rely on the translog not being flushed setting 'indices.memory.shard_inactive_time', '20m' - // debug logging for testRecovery - setting 'logger.level', 'DEBUG' - numNodes = 2 dataDir = { nodeNum -> oldClusterTest.nodes[nodeNum].dataDir } cleanShared = false // We want to keep snapshots made by the old cluster! diff --git a/x-pack/qa/full-cluster-restart/build.gradle b/x-pack/qa/full-cluster-restart/build.gradle index ab8f9172b69..65c4573e9a0 100644 --- a/x-pack/qa/full-cluster-restart/build.gradle +++ b/x-pack/qa/full-cluster-restart/build.gradle @@ -155,9 +155,6 @@ subprojects { // some tests rely on the translog not being flushed setting 'indices.memory.shard_inactive_time', '20m' - // debug logging for testRecovery see https://github.com/elastic/x-pack-elasticsearch/issues/2691 - setting 'logger.level', 'DEBUG' - setting 'xpack.security.enabled', 'true' setting 'xpack.security.transport.ssl.enabled', 'true' setting 'xpack.ssl.keystore.path', 'testnode.jks' @@ -201,9 +198,6 @@ subprojects { setupCommand 'setupTestUser', 'bin/elasticsearch-users', 'useradd', 'test_user', '-p', 'x-pack-test-password', '-r', 'superuser' waitCondition = waitWithAuth - // debug logging for testRecovery see https://github.com/elastic/x-pack-elasticsearch/issues/2691 - setting 'logger.level', 'DEBUG' - // some tests rely on the translog not being flushed setting 'indices.memory.shard_inactive_time', '20m' setting 'xpack.security.enabled', 'true' From 27e07ec859f392bf48280eb6d8fca9f88c4e8058 Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Tue, 11 Sep 2018 16:32:52 -0700 Subject: [PATCH 16/78] HLRC: ML Delete Forecast API (#33526) * HLRC: ML Delete Forecast API --- .../client/MLRequestConverters.java | 21 ++ .../client/MachineLearningClient.java | 53 +++++ .../client/ml/DeleteForecastRequest.java | 183 ++++++++++++++++++ .../client/MLRequestConvertersTests.java | 29 +++ .../client/MachineLearningIT.java | 73 +++++++ .../MlClientDocumentationIT.java | 81 ++++++++ .../client/ml/DeleteForecastRequestTests.java | 62 ++++++ .../high-level/ml/delete-forecast.asciidoc | 78 ++++++++ .../high-level/supported-apis.asciidoc | 2 + 9 files changed, 582 insertions(+) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteForecastRequest.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteForecastRequestTests.java create mode 100644 docs/java-rest/high-level/ml/delete-forecast.asciidoc diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java index d158c1a06a2..9504c394c69 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java @@ -28,6 +28,7 @@ import org.apache.http.entity.ByteArrayEntity; import org.apache.lucene.util.BytesRef; import org.elasticsearch.client.RequestConverters.EndpointBuilder; import org.elasticsearch.client.ml.CloseJobRequest; +import org.elasticsearch.client.ml.DeleteForecastRequest; import org.elasticsearch.client.ml.DeleteJobRequest; import org.elasticsearch.client.ml.FlushJobRequest; import org.elasticsearch.client.ml.ForecastJobRequest; @@ -181,6 +182,26 @@ final class MLRequestConverters { return request; } + static Request deleteForecast(DeleteForecastRequest deleteForecastRequest) throws IOException { + String endpoint = new EndpointBuilder() + .addPathPartAsIs("_xpack") + .addPathPartAsIs("ml") + .addPathPartAsIs("anomaly_detectors") + .addPathPart(deleteForecastRequest.getJobId()) + .addPathPartAsIs("_forecast") + .addPathPart(Strings.collectionToCommaDelimitedString(deleteForecastRequest.getForecastIds())) + .build(); + Request request = new Request(HttpDelete.METHOD_NAME, endpoint); + RequestConverters.Params params = new RequestConverters.Params(request); + if (deleteForecastRequest.isAllowNoForecasts() != null) { + params.putParam("allow_no_forecasts", Boolean.toString(deleteForecastRequest.isAllowNoForecasts())); + } + if (deleteForecastRequest.timeout() != null) { + params.putParam("timeout", deleteForecastRequest.timeout().getStringRep()); + } + return request; + } + static Request getBuckets(GetBucketsRequest getBucketsRequest) throws IOException { String endpoint = new EndpointBuilder() .addPathPartAsIs("_xpack") diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java index b5f7550b913..d42d2b58d44 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java @@ -19,6 +19,8 @@ package org.elasticsearch.client; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.client.ml.DeleteForecastRequest; import org.elasticsearch.client.ml.ForecastJobRequest; import org.elasticsearch.client.ml.ForecastJobResponse; import org.elasticsearch.client.ml.PostDataRequest; @@ -389,6 +391,11 @@ public final class MachineLearningClient { /** * Updates a Machine Learning {@link org.elasticsearch.client.ml.job.config.Job} * + *

+ * For additional info + * see + *

+ * * @param request the {@link UpdateJobRequest} object enclosing the desired updates * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return a PutJobResponse object containing the updated job object @@ -427,6 +434,10 @@ public final class MachineLearningClient { /** * Updates a Machine Learning {@link org.elasticsearch.client.ml.job.config.Job} asynchronously * + *

+ * For additional info + * see + *

* @param request the {@link UpdateJobRequest} object enclosing the desired updates * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @param listener Listener to be notified upon request completion @@ -440,6 +451,48 @@ public final class MachineLearningClient { Collections.emptySet()); } + /** + * Deletes Machine Learning Job Forecasts + * + *

+ * For additional info + * see + *

+ * + * @param request the {@link DeleteForecastRequest} object enclosing the desired jobId, forecastIDs, and other options + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return a AcknowledgedResponse object indicating request success + * @throws IOException when there is a serialization issue sending the request or receiving the response + */ + public AcknowledgedResponse deleteForecast(DeleteForecastRequest request, RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(request, + MLRequestConverters::deleteForecast, + options, + AcknowledgedResponse::fromXContent, + Collections.emptySet()); + } + + /** + * Deletes Machine Learning Job Forecasts asynchronously + * + *

+ * For additional info + * see + *

+ * + * @param request the {@link DeleteForecastRequest} object enclosing the desired jobId, forecastIDs, and other options + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener Listener to be notified upon request completion + */ + public void deleteForecastAsync(DeleteForecastRequest request, RequestOptions options, ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(request, + MLRequestConverters::deleteForecast, + options, + AcknowledgedResponse::fromXContent, + listener, + Collections.emptySet()); + } + /** * Gets the buckets for a Machine Learning Job. *

diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteForecastRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteForecastRequest.java new file mode 100644 index 00000000000..f7c8a6c0733 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteForecastRequest.java @@ -0,0 +1,183 @@ +/* + * 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; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.client.ml.job.config.Job; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * POJO for a delete forecast request + */ +public class DeleteForecastRequest extends ActionRequest implements ToXContentObject { + + public static final ParseField FORECAST_ID = new ParseField("forecast_id"); + public static final ParseField ALLOW_NO_FORECASTS = new ParseField("allow_no_forecasts"); + public static final ParseField TIMEOUT = new ParseField("timeout"); + public static final String ALL = "_all"; + + public static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("delete_forecast_request", (a) -> new DeleteForecastRequest((String) a[0])); + + static { + PARSER.declareString(ConstructingObjectParser.constructorArg(), Job.ID); + PARSER.declareStringOrNull( + (c, p) -> c.setForecastIds(Strings.commaDelimitedListToStringArray(p)), FORECAST_ID); + PARSER.declareBoolean(DeleteForecastRequest::setAllowNoForecasts, ALLOW_NO_FORECASTS); + PARSER.declareString(DeleteForecastRequest::timeout, TIMEOUT); + } + + /** + * Create a new {@link DeleteForecastRequest} that explicitly deletes all forecasts + * + * @param jobId the jobId of the Job whose forecasts to delete + */ + public static DeleteForecastRequest deleteAllForecasts(String jobId) { + DeleteForecastRequest request = new DeleteForecastRequest(jobId); + request.setForecastIds(ALL); + return request; + } + + private final String jobId; + private List forecastIds = new ArrayList<>(); + private Boolean allowNoForecasts; + private TimeValue timeout; + + /** + * Create a new DeleteForecastRequest for the given Job ID + * + * @param jobId the jobId of the Job whose forecast(s) to delete + */ + public DeleteForecastRequest(String jobId) { + this.jobId = Objects.requireNonNull(jobId, Job.ID.getPreferredName()); + } + + public String getJobId() { + return jobId; + } + + public List getForecastIds() { + return forecastIds; + } + + /** + * The forecast IDs to delete. Can be also be {@link DeleteForecastRequest#ALL} to explicitly delete ALL forecasts + * + * @param forecastIds forecast IDs to delete + */ + public void setForecastIds(String... forecastIds) { + setForecastIds(Arrays.asList(forecastIds)); + } + + void setForecastIds(List forecastIds) { + if (forecastIds.stream().anyMatch(Objects::isNull)) { + throw new NullPointerException("forecastIds must not contain null values"); + } + this.forecastIds = new ArrayList<>(forecastIds); + } + + public Boolean isAllowNoForecasts() { + return allowNoForecasts; + } + + /** + * Sets the `allow_no_forecasts` field. + * + * @param allowNoForecasts when {@code true} no error is thrown when {@link DeleteForecastRequest#ALL} does not find any forecasts + */ + public void setAllowNoForecasts(boolean allowNoForecasts) { + this.allowNoForecasts = allowNoForecasts; + } + + /** + * Allows to set the timeout + * @param timeout timeout as a string (e.g. 1s) + */ + public void timeout(String timeout) { + this.timeout = TimeValue.parseTimeValue(timeout, this.timeout, getClass().getSimpleName() + ".timeout"); + } + + /** + * Allows to set the timeout + * @param timeout timeout as a {@link TimeValue} + */ + public void timeout(TimeValue timeout) { + this.timeout = timeout; + } + + public TimeValue timeout() { + return timeout; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (other == null || getClass() != other.getClass()) { + return false; + } + + DeleteForecastRequest that = (DeleteForecastRequest) other; + return Objects.equals(jobId, that.jobId) && + Objects.equals(forecastIds, that.forecastIds) && + Objects.equals(allowNoForecasts, that.allowNoForecasts) && + Objects.equals(timeout, that.timeout); + } + + @Override + public int hashCode() { + return Objects.hash(jobId, forecastIds, allowNoForecasts, timeout); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(Job.ID.getPreferredName(), jobId); + if (forecastIds != null) { + builder.field(FORECAST_ID.getPreferredName(), Strings.collectionToCommaDelimitedString(forecastIds)); + } + if (allowNoForecasts != null) { + builder.field(ALLOW_NO_FORECASTS.getPreferredName(), allowNoForecasts); + } + if (timeout != null) { + builder.field(TIMEOUT.getPreferredName(), timeout.getStringRep()); + } + builder.endObject(); + return builder; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java index 7cc5f119c39..d63573b534c 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java @@ -24,6 +24,7 @@ import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.elasticsearch.client.ml.CloseJobRequest; +import org.elasticsearch.client.ml.DeleteForecastRequest; import org.elasticsearch.client.ml.DeleteJobRequest; import org.elasticsearch.client.ml.FlushJobRequest; import org.elasticsearch.client.ml.ForecastJobRequest; @@ -44,6 +45,7 @@ import org.elasticsearch.client.ml.job.config.Job; import org.elasticsearch.client.ml.job.config.JobUpdate; import org.elasticsearch.client.ml.job.config.JobUpdateTests; import org.elasticsearch.client.ml.job.util.PageParams; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; @@ -204,6 +206,33 @@ public class MLRequestConvertersTests extends ESTestCase { } } + public void testDeleteForecast() throws Exception { + String jobId = randomAlphaOfLength(10); + DeleteForecastRequest deleteForecastRequest = new DeleteForecastRequest(jobId); + + Request request = MLRequestConverters.deleteForecast(deleteForecastRequest); + assertEquals(HttpDelete.METHOD_NAME, request.getMethod()); + assertEquals("/_xpack/ml/anomaly_detectors/" + jobId + "/_forecast", request.getEndpoint()); + assertFalse(request.getParameters().containsKey("timeout")); + assertFalse(request.getParameters().containsKey("allow_no_forecasts")); + + deleteForecastRequest.setForecastIds(randomAlphaOfLength(10), randomAlphaOfLength(10)); + deleteForecastRequest.timeout("10s"); + deleteForecastRequest.setAllowNoForecasts(true); + + request = MLRequestConverters.deleteForecast(deleteForecastRequest); + assertEquals( + "/_xpack/ml/anomaly_detectors/" + + jobId + + "/_forecast/" + + Strings.collectionToCommaDelimitedString(deleteForecastRequest.getForecastIds()), + request.getEndpoint()); + assertEquals("10s", + request.getParameters().get(DeleteForecastRequest.TIMEOUT.getPreferredName())); + assertEquals(Boolean.toString(true), + request.getParameters().get(DeleteForecastRequest.ALLOW_NO_FORECASTS.getPreferredName())); + } + public void testGetBuckets() throws IOException { String jobId = randomAlphaOfLength(10); GetBucketsRequest getBucketsRequest = new GetBucketsRequest(jobId); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java index fb715683b27..db680aaa95d 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java @@ -20,6 +20,10 @@ package org.elasticsearch.client; import com.carrotsearch.randomizedtesting.generators.CodepointSetGenerator; import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.action.get.GetRequest; +import org.elasticsearch.action.get.GetResponse; +import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.client.ml.DeleteForecastRequest; import org.elasticsearch.client.ml.ForecastJobRequest; import org.elasticsearch.client.ml.ForecastJobResponse; import org.elasticsearch.client.ml.PostDataRequest; @@ -288,6 +292,75 @@ public class MachineLearningIT extends ESRestHighLevelClientTestCase { assertEquals("Updated description", getResponse.jobs().get(0).getDescription()); } + public void testDeleteForecast() throws Exception { + String jobId = "test-delete-forecast"; + + Job job = buildJob(jobId); + MachineLearningClient machineLearningClient = highLevelClient().machineLearning(); + machineLearningClient.putJob(new PutJobRequest(job), RequestOptions.DEFAULT); + machineLearningClient.openJob(new OpenJobRequest(jobId), RequestOptions.DEFAULT); + + Job noForecastsJob = buildJob("test-delete-forecast-none"); + machineLearningClient.putJob(new PutJobRequest(noForecastsJob), RequestOptions.DEFAULT); + + PostDataRequest.JsonBuilder builder = new PostDataRequest.JsonBuilder(); + for(int i = 0; i < 30; i++) { + Map hashMap = new HashMap<>(); + hashMap.put("total", randomInt(1000)); + hashMap.put("timestamp", (i+1)*1000); + builder.addDoc(hashMap); + } + + PostDataRequest postDataRequest = new PostDataRequest(jobId, builder); + machineLearningClient.postData(postDataRequest, RequestOptions.DEFAULT); + machineLearningClient.flushJob(new FlushJobRequest(jobId), RequestOptions.DEFAULT); + ForecastJobResponse forecastJobResponse1 = machineLearningClient.forecastJob(new ForecastJobRequest(jobId), RequestOptions.DEFAULT); + ForecastJobResponse forecastJobResponse2 = machineLearningClient.forecastJob(new ForecastJobRequest(jobId), RequestOptions.DEFAULT); + waitForForecastToComplete(jobId, forecastJobResponse1.getForecastId()); + waitForForecastToComplete(jobId, forecastJobResponse2.getForecastId()); + + { + DeleteForecastRequest request = new DeleteForecastRequest(jobId); + request.setForecastIds(forecastJobResponse1.getForecastId(), forecastJobResponse2.getForecastId()); + AcknowledgedResponse response = execute(request, machineLearningClient::deleteForecast, + machineLearningClient::deleteForecastAsync); + assertTrue(response.isAcknowledged()); + assertFalse(forecastExists(jobId, forecastJobResponse1.getForecastId())); + assertFalse(forecastExists(jobId, forecastJobResponse2.getForecastId())); + } + { + DeleteForecastRequest request = DeleteForecastRequest.deleteAllForecasts(noForecastsJob.getId()); + request.setAllowNoForecasts(true); + AcknowledgedResponse response = execute(request, machineLearningClient::deleteForecast, + machineLearningClient::deleteForecastAsync); + assertTrue(response.isAcknowledged()); + } + { + DeleteForecastRequest request = DeleteForecastRequest.deleteAllForecasts(noForecastsJob.getId()); + request.setAllowNoForecasts(false); + ElasticsearchStatusException exception = expectThrows(ElasticsearchStatusException.class, + () -> execute(request, machineLearningClient::deleteForecast, machineLearningClient::deleteForecastAsync)); + assertThat(exception.status().getStatus(), equalTo(404)); + } + } + + private void waitForForecastToComplete(String jobId, String forecastId) throws Exception { + GetRequest request = new GetRequest(".ml-anomalies-" + jobId); + request.id(jobId + "_model_forecast_request_stats_" + forecastId); + assertBusy(() -> { + GetResponse getResponse = highLevelClient().get(request, RequestOptions.DEFAULT); + assertTrue(getResponse.isExists()); + assertTrue(getResponse.getSourceAsString().contains("finished")); + }, 30, TimeUnit.SECONDS); + } + + private boolean forecastExists(String jobId, String forecastId) throws Exception { + GetRequest getRequest = new GetRequest(".ml-anomalies-" + jobId); + getRequest.id(jobId + "_model_forecast_request_stats_" + forecastId); + GetResponse getResponse = highLevelClient().get(getRequest, RequestOptions.DEFAULT); + return getResponse.isExists(); + } + public static String randomValidJobId() { CodepointSetGenerator generator = new CodepointSetGenerator("abcdefghijklmnopqrstuvwxyz0123456789".toCharArray()); return generator.ofCodePointsLength(random(), 10, 10); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java index 845729eccbd..2da0da0c53f 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java @@ -21,8 +21,11 @@ package org.elasticsearch.client.documentation; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.LatchedActionListener; import org.elasticsearch.action.bulk.BulkRequest; +import org.elasticsearch.action.get.GetRequest; +import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.action.support.master.AcknowledgedResponse; import org.elasticsearch.client.ESRestHighLevelClientTestCase; import org.elasticsearch.client.MachineLearningGetResultsIT; import org.elasticsearch.client.MachineLearningIT; @@ -31,6 +34,7 @@ import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.client.ml.CloseJobRequest; import org.elasticsearch.client.ml.CloseJobResponse; +import org.elasticsearch.client.ml.DeleteForecastRequest; import org.elasticsearch.client.ml.DeleteJobRequest; import org.elasticsearch.client.ml.DeleteJobResponse; import org.elasticsearch.client.ml.FlushJobRequest; @@ -639,8 +643,85 @@ public class MlClientDocumentationIT extends ESRestHighLevelClientTestCase { assertTrue(latch.await(30L, TimeUnit.SECONDS)); } } + + public void testDeleteForecast() throws Exception { + RestHighLevelClient client = highLevelClient(); + Job job = MachineLearningIT.buildJob("deleting-forecast-for-job"); + client.machineLearning().putJob(new PutJobRequest(job), RequestOptions.DEFAULT); + client.machineLearning().openJob(new OpenJobRequest(job.getId()), RequestOptions.DEFAULT); + PostDataRequest.JsonBuilder builder = new PostDataRequest.JsonBuilder(); + for(int i = 0; i < 30; i++) { + Map hashMap = new HashMap<>(); + hashMap.put("total", randomInt(1000)); + hashMap.put("timestamp", (i+1)*1000); + builder.addDoc(hashMap); + } + PostDataRequest postDataRequest = new PostDataRequest(job.getId(), builder); + client.machineLearning().postData(postDataRequest, RequestOptions.DEFAULT); + client.machineLearning().flushJob(new FlushJobRequest(job.getId()), RequestOptions.DEFAULT); + ForecastJobResponse forecastJobResponse = client.machineLearning(). + forecastJob(new ForecastJobRequest(job.getId()), RequestOptions.DEFAULT); + String forecastId = forecastJobResponse.getForecastId(); + + GetRequest request = new GetRequest(".ml-anomalies-" + job.getId()); + request.id(job.getId() + "_model_forecast_request_stats_" + forecastId); + assertBusy(() -> { + GetResponse getResponse = highLevelClient().get(request, RequestOptions.DEFAULT); + assertTrue(getResponse.isExists()); + assertTrue(getResponse.getSourceAsString().contains("finished")); + }, 30, TimeUnit.SECONDS); + + { + //tag::x-pack-ml-delete-forecast-request + DeleteForecastRequest deleteForecastRequest = new DeleteForecastRequest("deleting-forecast-for-job"); //<1> + //end::x-pack-ml-delete-forecast-request + + //tag::x-pack-ml-delete-forecast-request-options + deleteForecastRequest.setForecastIds(forecastId); //<1> + deleteForecastRequest.timeout("30s"); //<2> + deleteForecastRequest.setAllowNoForecasts(true); //<3> + //end::x-pack-ml-delete-forecast-request-options + + //tag::x-pack-ml-delete-forecast-execute + AcknowledgedResponse deleteForecastResponse = client.machineLearning().deleteForecast(deleteForecastRequest, + RequestOptions.DEFAULT); + //end::x-pack-ml-delete-forecast-execute + + //tag::x-pack-ml-delete-forecast-response + boolean isAcknowledged = deleteForecastResponse.isAcknowledged(); //<1> + //end::x-pack-ml-delete-forecast-response + } + { + //tag::x-pack-ml-delete-forecast-listener + ActionListener listener = new ActionListener() { + @Override + public void onResponse(AcknowledgedResponse DeleteForecastResponse) { + //<1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + //end::x-pack-ml-delete-forecast-listener + DeleteForecastRequest deleteForecastRequest = DeleteForecastRequest.deleteAllForecasts(job.getId()); + deleteForecastRequest.setAllowNoForecasts(true); + + // Replace the empty listener by a blocking listener in test + final CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + + // tag::x-pack-ml-delete-forecast-execute-async + client.machineLearning().deleteForecastAsync(deleteForecastRequest, RequestOptions.DEFAULT, listener); //<1> + // end::x-pack-ml-delete-forecast-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + } + public void testGetJobStats() throws Exception { RestHighLevelClient client = highLevelClient(); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteForecastRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteForecastRequestTests.java new file mode 100644 index 00000000000..ad012277711 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteForecastRequestTests.java @@ -0,0 +1,62 @@ +/* + * 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; + +import org.elasticsearch.client.ml.job.config.JobTests; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class DeleteForecastRequestTests extends AbstractXContentTestCase { + + @Override + protected DeleteForecastRequest createTestInstance() { + + DeleteForecastRequest deleteForecastRequest = new DeleteForecastRequest(JobTests.randomValidJobId()); + if (randomBoolean()) { + int length = randomInt(10); + List ids = new ArrayList<>(length); + for(int i = 0; i < length; i++) { + ids.add(randomAlphaOfLength(10)); + } + deleteForecastRequest.setForecastIds(ids); + } + if (randomBoolean()) { + deleteForecastRequest.setAllowNoForecasts(randomBoolean()); + } + if (randomBoolean()) { + deleteForecastRequest.timeout(randomTimeValue()); + } + return deleteForecastRequest; + } + + @Override + protected DeleteForecastRequest doParseInstance(XContentParser parser) throws IOException { + return DeleteForecastRequest.PARSER.apply(parser, null); + } + + @Override + protected boolean supportsUnknownFields() { + return false; + } + +} diff --git a/docs/java-rest/high-level/ml/delete-forecast.asciidoc b/docs/java-rest/high-level/ml/delete-forecast.asciidoc new file mode 100644 index 00000000000..09aa5c734ff --- /dev/null +++ b/docs/java-rest/high-level/ml/delete-forecast.asciidoc @@ -0,0 +1,78 @@ +[[java-rest-high-x-pack-ml-delete-forecast]] +=== Delete Forecast API + +The Delete Forecast API provides the ability to delete a {ml} job's +forecast in the cluster. +It accepts a `DeleteForecastRequest` object and responds +with an `AcknowledgedResponse` object. + +[[java-rest-high-x-pack-ml-delete-forecast-request]] +==== Delete Forecast Request + +A `DeleteForecastRequest` object gets created with an existing non-null `jobId`. +All other fields are optional for the request. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-delete-forecast-request] +-------------------------------------------------- +<1> Constructing a new request referencing an existing `jobId` + +==== Optional Arguments + +The following arguments are optional. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-delete-forecast-request-options] +-------------------------------------------------- +<1> Sets the specific forecastIds to delete, can be set to `_all` to indicate ALL forecasts for the given +`jobId` +<2> Set the timeout for the request to respond, default is 30 seconds +<3> Set the `allow_no_forecasts` option. When `true` no error will be returned if an `_all` +request finds no forecasts. It defaults to `true` + +[[java-rest-high-x-pack-ml-delete-forecast-execution]] +==== Execution + +The request can be executed through the `MachineLearningClient` contained +in the `RestHighLevelClient` object, accessed via the `machineLearningClient()` method. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-delete-forecast-execute] +-------------------------------------------------- + +[[java-rest-high-x-pack-ml-delete-forecast-execution-async]] +==== Asynchronous Execution + +The request can also be executed asynchronously: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-delete-forecast-execute-async] +-------------------------------------------------- +<1> The `DeleteForecastRequest` to execute and the `ActionListener` to use when +the execution completes + +The method does not block and returns immediately. The passed `ActionListener` is used +to notify the caller of completion. A typical `ActionListener` for `AcknowledgedResponse` may +look like + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-delete-forecast-listener] +-------------------------------------------------- +<1> `onResponse` is called back when the action is completed successfully +<2> `onFailure` is called back when some unexpected error occurs + +[[java-rest-high-x-pack-ml-delete-forecast-response]] +==== Delete Forecast Response + +An `AcknowledgedResponse` contains an acknowledgement of the forecast(s) deletion + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-delete-forecast-response] +-------------------------------------------------- +<1> `isAcknowledged()` indicates if the forecast was successfully deleted or not. diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index 87639a2ea3f..a6d173f6e27 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -221,6 +221,7 @@ The Java High Level REST Client supports the following Machine Learning APIs: * <> * <> * <> +* <> * <> * <> * <> @@ -237,6 +238,7 @@ include::ml/update-job.asciidoc[] include::ml/flush-job.asciidoc[] include::ml/get-job-stats.asciidoc[] include::ml/forecast-job.asciidoc[] +include::ml/delete-forecast.asciidoc[] include::ml/get-buckets.asciidoc[] include::ml/get-overall-buckets.asciidoc[] include::ml/get-records.asciidoc[] From 743327efc204c3e3c7e26567f285c5c5032f30a7 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Tue, 11 Sep 2018 22:09:37 -0400 Subject: [PATCH 17/78] Reset replica engine to global checkpoint on promotion (#33473) When a replica starts following a newly promoted primary, it may have some operations which don't exist on the new primary. Thus we need to throw those operations to align a replica with the new primary. This can be done by first resetting an engine from the safe commit, then replaying the local translog up to the global checkpoint. Relates #32867 --- .../elasticsearch/index/engine/Engine.java | 13 ++- .../index/engine/InternalEngine.java | 12 +-- .../index/engine/ReadOnlyEngine.java | 4 - .../index/seqno/LocalCheckpointTracker.java | 1 + .../elasticsearch/index/shard/IndexShard.java | 84 ++++++++++++------- .../discovery/AbstractDisruptionTestCase.java | 1 + .../index/engine/InternalEngineTests.java | 2 +- .../index/engine/ReadOnlyEngineTests.java | 4 +- .../IndexLevelReplicationTests.java | 8 +- .../RecoveryDuringReplicationTests.java | 31 +------ .../index/shard/IndexShardTests.java | 68 ++++++++++----- .../elasticsearch/recovery/RelocationIT.java | 1 + .../index/engine/DocIdSeqNoAndTerm.java | 66 +++++++++++++++ .../index/engine/EngineTestCase.java | 29 +++++-- .../index/shard/IndexShardTestCase.java | 26 ++++-- .../elasticsearch/test/ESIntegTestCase.java | 45 ++++++++++ 16 files changed, 274 insertions(+), 121 deletions(-) create mode 100644 test/framework/src/main/java/org/elasticsearch/index/engine/DocIdSeqNoAndTerm.java diff --git a/server/src/main/java/org/elasticsearch/index/engine/Engine.java b/server/src/main/java/org/elasticsearch/index/engine/Engine.java index ea8161c1589..5ebe13577f4 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/Engine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/Engine.java @@ -678,12 +678,6 @@ public abstract class Engine implements Closeable { */ public abstract void waitForOpsToComplete(long seqNo) throws InterruptedException; - /** - * Reset the local checkpoint in the tracker to the given local checkpoint - * @param localCheckpoint the new checkpoint to be set - */ - public abstract void resetLocalCheckpoint(long localCheckpoint); - /** * @return a {@link SeqNoStats} object, using local state and the supplied global checkpoint */ @@ -1165,11 +1159,16 @@ public abstract class Engine implements Closeable { PRIMARY, REPLICA, PEER_RECOVERY, - LOCAL_TRANSLOG_RECOVERY; + LOCAL_TRANSLOG_RECOVERY, + LOCAL_RESET; public boolean isRecovery() { return this == PEER_RECOVERY || this == LOCAL_TRANSLOG_RECOVERY; } + + boolean isFromTranslog() { + return this == LOCAL_TRANSLOG_RECOVERY || this == LOCAL_RESET; + } } public Origin origin() { diff --git a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java index e8f5e415908..52dd4d3fcd0 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java @@ -729,6 +729,7 @@ public class InternalEngine extends Engine { : "version: " + index.version() + " type: " + index.versionType(); return true; case LOCAL_TRANSLOG_RECOVERY: + case LOCAL_RESET: assert index.isRetry(); return true; // allow to optimize in order to update the max safe time stamp default: @@ -827,7 +828,7 @@ public class InternalEngine extends Engine { indexResult = new IndexResult( plan.versionForIndexing, getPrimaryTerm(), plan.seqNoForIndexing, plan.currentNotFoundOrDeleted); } - if (index.origin() != Operation.Origin.LOCAL_TRANSLOG_RECOVERY) { + if (index.origin().isFromTranslog() == false) { final Translog.Location location; if (indexResult.getResultType() == Result.Type.SUCCESS) { location = translog.add(new Translog.Index(index, indexResult)); @@ -1167,7 +1168,7 @@ public class InternalEngine extends Engine { deleteResult = new DeleteResult( plan.versionOfDeletion, getPrimaryTerm(), plan.seqNoOfDeletion, plan.currentlyDeleted == false); } - if (delete.origin() != Operation.Origin.LOCAL_TRANSLOG_RECOVERY) { + if (delete.origin().isFromTranslog() == false) { final Translog.Location location; if (deleteResult.getResultType() == Result.Type.SUCCESS) { location = translog.add(new Translog.Delete(delete, deleteResult)); @@ -1405,7 +1406,7 @@ public class InternalEngine extends Engine { } } final NoOpResult noOpResult = failure != null ? new NoOpResult(getPrimaryTerm(), noOp.seqNo(), failure) : new NoOpResult(getPrimaryTerm(), noOp.seqNo()); - if (noOp.origin() != Operation.Origin.LOCAL_TRANSLOG_RECOVERY) { + if (noOp.origin().isFromTranslog() == false) { final Translog.Location location = translog.add(new Translog.NoOp(noOp.seqNo(), noOp.primaryTerm(), noOp.reason())); noOpResult.setTranslogLocation(location); } @@ -2324,11 +2325,6 @@ public class InternalEngine extends Engine { localCheckpointTracker.waitForOpsToComplete(seqNo); } - @Override - public void resetLocalCheckpoint(long localCheckpoint) { - localCheckpointTracker.resetCheckpoint(localCheckpoint); - } - @Override public SeqNoStats getSeqNoStats(long globalCheckpoint) { return localCheckpointTracker.getStats(globalCheckpoint); diff --git a/server/src/main/java/org/elasticsearch/index/engine/ReadOnlyEngine.java b/server/src/main/java/org/elasticsearch/index/engine/ReadOnlyEngine.java index a55987d0a00..b958bd84b76 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/ReadOnlyEngine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/ReadOnlyEngine.java @@ -257,10 +257,6 @@ public final class ReadOnlyEngine extends Engine { public void waitForOpsToComplete(long seqNo) { } - @Override - public void resetLocalCheckpoint(long newCheckpoint) { - } - @Override public SeqNoStats getSeqNoStats(long globalCheckpoint) { return new SeqNoStats(seqNoStats.getMaxSeqNo(), seqNoStats.getLocalCheckpoint(), globalCheckpoint); diff --git a/server/src/main/java/org/elasticsearch/index/seqno/LocalCheckpointTracker.java b/server/src/main/java/org/elasticsearch/index/seqno/LocalCheckpointTracker.java index cd33c1bf046..9fad96940b8 100644 --- a/server/src/main/java/org/elasticsearch/index/seqno/LocalCheckpointTracker.java +++ b/server/src/main/java/org/elasticsearch/index/seqno/LocalCheckpointTracker.java @@ -109,6 +109,7 @@ public class LocalCheckpointTracker { * @param checkpoint the local checkpoint to reset this tracker to */ public synchronized void resetCheckpoint(final long checkpoint) { + // TODO: remove this method as after we restore the local history on promotion. assert checkpoint != SequenceNumbers.UNASSIGNED_SEQ_NO; assert checkpoint <= this.checkpoint; processedSeqNo.clear(); diff --git a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java index bceb106aeef..4bb56c8b0d3 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java +++ b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java @@ -163,7 +163,6 @@ import java.util.stream.Collectors; import java.util.stream.StreamSupport; import static org.elasticsearch.index.mapper.SourceToParse.source; -import static org.elasticsearch.index.seqno.SequenceNumbers.NO_OPS_PERFORMED; import static org.elasticsearch.index.seqno.SequenceNumbers.UNASSIGNED_SEQ_NO; public class IndexShard extends AbstractIndexShardComponent implements IndicesClusterStateService.Shard { @@ -1273,16 +1272,18 @@ public class IndexShard extends AbstractIndexShardComponent implements IndicesCl return result; } - // package-private for testing - int runTranslogRecovery(Engine engine, Translog.Snapshot snapshot) throws IOException { - recoveryState.getTranslog().totalOperations(snapshot.totalOperations()); - recoveryState.getTranslog().totalOperationsOnStart(snapshot.totalOperations()); + /** + * Replays translog operations from the provided translog {@code snapshot} to the current engine using the given {@code origin}. + * The callback {@code onOperationRecovered} is notified after each translog operation is replayed successfully. + */ + int runTranslogRecovery(Engine engine, Translog.Snapshot snapshot, Engine.Operation.Origin origin, + Runnable onOperationRecovered) throws IOException { int opsRecovered = 0; Translog.Operation operation; while ((operation = snapshot.next()) != null) { try { logger.trace("[translog] recover op {}", operation); - Engine.Result result = applyTranslogOperation(operation, Engine.Operation.Origin.LOCAL_TRANSLOG_RECOVERY); + Engine.Result result = applyTranslogOperation(operation, origin); switch (result.getResultType()) { case FAILURE: throw result.getFailure(); @@ -1295,7 +1296,7 @@ public class IndexShard extends AbstractIndexShardComponent implements IndicesCl } opsRecovered++; - recoveryState.getTranslog().incrementRecoveredOperations(); + onOperationRecovered.run(); } catch (Exception e) { if (ExceptionsHelper.status(e) == RestStatus.BAD_REQUEST) { // mainly for MapperParsingException and Failure to detect xcontent @@ -1313,8 +1314,15 @@ public class IndexShard extends AbstractIndexShardComponent implements IndicesCl * Operations from the translog will be replayed to bring lucene up to date. **/ public void openEngineAndRecoverFromTranslog() throws IOException { + final RecoveryState.Translog translogRecoveryStats = recoveryState.getTranslog(); + final Engine.TranslogRecoveryRunner translogRecoveryRunner = (engine, snapshot) -> { + translogRecoveryStats.totalOperations(snapshot.totalOperations()); + translogRecoveryStats.totalOperationsOnStart(snapshot.totalOperations()); + return runTranslogRecovery(engine, snapshot, Engine.Operation.Origin.LOCAL_TRANSLOG_RECOVERY, + translogRecoveryStats::incrementRecoveredOperations); + }; innerOpenEngineAndTranslog(); - getEngine().recoverFromTranslog(this::runTranslogRecovery, Long.MAX_VALUE); + getEngine().recoverFromTranslog(translogRecoveryRunner, Long.MAX_VALUE); } /** @@ -1352,11 +1360,7 @@ public class IndexShard extends AbstractIndexShardComponent implements IndicesCl final String translogUUID = store.readLastCommittedSegmentsInfo().getUserData().get(Translog.TRANSLOG_UUID_KEY); final long globalCheckpoint = Translog.readGlobalCheckpoint(translogConfig.getTranslogPath(), translogUUID); replicationTracker.updateGlobalCheckpointOnReplica(globalCheckpoint, "read from translog checkpoint"); - - assertMaxUnsafeAutoIdInCommit(); - - final long minRetainedTranslogGen = Translog.readMinTranslogGeneration(translogConfig.getTranslogPath(), translogUUID); - store.trimUnsafeCommits(globalCheckpoint, minRetainedTranslogGen, config.getIndexSettings().getIndexVersionCreated()); + trimUnsafeCommits(); createNewEngine(config); verifyNotClosed(); @@ -1367,6 +1371,15 @@ public class IndexShard extends AbstractIndexShardComponent implements IndicesCl assert recoveryState.getStage() == RecoveryState.Stage.TRANSLOG : "TRANSLOG stage expected but was: " + recoveryState.getStage(); } + private void trimUnsafeCommits() throws IOException { + assert currentEngineReference.get() == null : "engine is running"; + final String translogUUID = store.readLastCommittedSegmentsInfo().getUserData().get(Translog.TRANSLOG_UUID_KEY); + final long globalCheckpoint = Translog.readGlobalCheckpoint(translogConfig.getTranslogPath(), translogUUID); + final long minRetainedTranslogGen = Translog.readMinTranslogGeneration(translogConfig.getTranslogPath(), translogUUID); + assertMaxUnsafeAutoIdInCommit(); + store.trimUnsafeCommits(globalCheckpoint, minRetainedTranslogGen, indexSettings.getIndexVersionCreated()); + } + private boolean assertSequenceNumbersInCommit() throws IOException { final Map userData = SegmentInfos.readLatestCommit(store.directory()).getUserData(); assert userData.containsKey(SequenceNumbers.LOCAL_CHECKPOINT_KEY) : "commit point doesn't contains a local checkpoint"; @@ -1463,7 +1476,7 @@ public class IndexShard extends AbstractIndexShardComponent implements IndicesCl if (origin == Engine.Operation.Origin.PRIMARY) { assert assertPrimaryMode(); } else { - assert origin == Engine.Operation.Origin.REPLICA; + assert origin == Engine.Operation.Origin.REPLICA || origin == Engine.Operation.Origin.LOCAL_RESET; assert assertReplicationTarget(); } if (writeAllowedStates.contains(state) == false) { @@ -2166,9 +2179,7 @@ public class IndexShard extends AbstractIndexShardComponent implements IndicesCl private Engine createNewEngine(EngineConfig config) { synchronized (mutex) { - if (state == IndexShardState.CLOSED) { - throw new AlreadyClosedException(shardId + " can't create engine - shard is closed"); - } + verifyNotClosed(); assert this.currentEngineReference.get() == null; Engine engine = newEngine(config); onNewEngine(engine); // call this before we pass the memory barrier otherwise actions that happen @@ -2314,19 +2325,14 @@ public class IndexShard extends AbstractIndexShardComponent implements IndicesCl bumpPrimaryTerm(opPrimaryTerm, () -> { updateGlobalCheckpointOnReplica(globalCheckpoint, "primary term transition"); final long currentGlobalCheckpoint = getGlobalCheckpoint(); - final long localCheckpoint; - if (currentGlobalCheckpoint == UNASSIGNED_SEQ_NO) { - localCheckpoint = NO_OPS_PERFORMED; + final long maxSeqNo = seqNoStats().getMaxSeqNo(); + logger.info("detected new primary with primary term [{}], global checkpoint [{}], max_seq_no [{}]", + opPrimaryTerm, currentGlobalCheckpoint, maxSeqNo); + if (currentGlobalCheckpoint < maxSeqNo) { + resetEngineToGlobalCheckpoint(); } else { - localCheckpoint = currentGlobalCheckpoint; + getEngine().rollTranslogGeneration(); } - logger.trace( - "detected new primary with primary term [{}], resetting local checkpoint from [{}] to [{}]", - opPrimaryTerm, - getLocalCheckpoint(), - localCheckpoint); - getEngine().resetLocalCheckpoint(localCheckpoint); - getEngine().rollTranslogGeneration(); }); } } @@ -2687,4 +2693,26 @@ public class IndexShard extends AbstractIndexShardComponent implements IndicesCl } }; } + + /** + * Rollback the current engine to the safe commit, then replay local translog up to the global checkpoint. + */ + void resetEngineToGlobalCheckpoint() throws IOException { + assert getActiveOperationsCount() == 0 : "Ongoing writes [" + getActiveOperations() + "]"; + sync(); // persist the global checkpoint to disk + final long globalCheckpoint = getGlobalCheckpoint(); + final Engine newEngine; + synchronized (mutex) { + verifyNotClosed(); + IOUtils.close(currentEngineReference.getAndSet(null)); + trimUnsafeCommits(); + newEngine = createNewEngine(newEngineConfig()); + active.set(true); + } + final Engine.TranslogRecoveryRunner translogRunner = (engine, snapshot) -> runTranslogRecovery( + engine, snapshot, Engine.Operation.Origin.LOCAL_RESET, () -> { + // TODO: add a dedicate recovery stats for the reset translog + }); + newEngine.recoverFromTranslog(translogRunner, globalCheckpoint); + } } diff --git a/server/src/test/java/org/elasticsearch/discovery/AbstractDisruptionTestCase.java b/server/src/test/java/org/elasticsearch/discovery/AbstractDisruptionTestCase.java index ac2f2b0d4f3..c0b01eb5ec5 100644 --- a/server/src/test/java/org/elasticsearch/discovery/AbstractDisruptionTestCase.java +++ b/server/src/test/java/org/elasticsearch/discovery/AbstractDisruptionTestCase.java @@ -111,6 +111,7 @@ public abstract class AbstractDisruptionTestCase extends ESIntegTestCase { super.beforeIndexDeletion(); internalCluster().assertConsistentHistoryBetweenTranslogAndLuceneIndex(); assertSeqNos(); + assertSameDocIdsOnShards(); } } diff --git a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java index 39132d805b2..8f9d90154f8 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java @@ -4087,7 +4087,7 @@ public class InternalEngineTests extends EngineTestCase { final long currentLocalCheckpoint = actualEngine.getLocalCheckpoint(); final long resetLocalCheckpoint = randomIntBetween(Math.toIntExact(SequenceNumbers.NO_OPS_PERFORMED), Math.toIntExact(currentLocalCheckpoint)); - actualEngine.resetLocalCheckpoint(resetLocalCheckpoint); + actualEngine.getLocalCheckpointTracker().resetCheckpoint(resetLocalCheckpoint); completedSeqNos.clear(); actualEngine.restoreLocalCheckpointFromTranslog(); final Set intersection = new HashSet<>(expectedCompletedSeqNos); diff --git a/server/src/test/java/org/elasticsearch/index/engine/ReadOnlyEngineTests.java b/server/src/test/java/org/elasticsearch/index/engine/ReadOnlyEngineTests.java index 4a5b89351bd..4080dd33d53 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/ReadOnlyEngineTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/ReadOnlyEngineTests.java @@ -27,7 +27,7 @@ import org.elasticsearch.index.seqno.SequenceNumbers; import org.elasticsearch.index.store.Store; import java.io.IOException; -import java.util.Set; +import java.util.List; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; @@ -43,7 +43,7 @@ public class ReadOnlyEngineTests extends EngineTestCase { EngineConfig config = config(defaultSettings, store, createTempDir(), newMergePolicy(), null, null, globalCheckpoint::get); int numDocs = scaledRandomIntBetween(10, 1000); final SeqNoStats lastSeqNoStats; - final Set lastDocIds; + final List lastDocIds; try (InternalEngine engine = createEngine(config)) { Engine.Get get = null; for (int i = 0; i < numDocs; i++) { diff --git a/server/src/test/java/org/elasticsearch/index/replication/IndexLevelReplicationTests.java b/server/src/test/java/org/elasticsearch/index/replication/IndexLevelReplicationTests.java index e471874f6d6..f2cdfbf8fc5 100644 --- a/server/src/test/java/org/elasticsearch/index/replication/IndexLevelReplicationTests.java +++ b/server/src/test/java/org/elasticsearch/index/replication/IndexLevelReplicationTests.java @@ -519,18 +519,14 @@ public class IndexLevelReplicationTests extends ESIndexLevelReplicationTestCase shards.promoteReplicaToPrimary(replica2).get(); logger.info("--> Recover replica3 from replica2"); recoverReplica(replica3, replica2, true); - try (Translog.Snapshot snapshot = getTranslog(replica3).newSnapshot()) { + try (Translog.Snapshot snapshot = replica3.getHistoryOperations("test", 0)) { assertThat(snapshot.totalOperations(), equalTo(initDocs + 1)); final List expectedOps = new ArrayList<>(initOperations); expectedOps.add(op2); assertThat(snapshot, containsOperationsInAnyOrder(expectedOps)); assertThat("Peer-recovery should not send overridden operations", snapshot.skippedOperations(), equalTo(0)); } - // TODO: We should assert the content of shards in the ReplicationGroup. - // Without rollback replicas(current implementation), we don't have the same content across shards: - // - replica1 has {doc1} - // - replica2 has {doc1, doc2} - // - replica3 can have either {doc2} only if operation-based recovery or {doc1, doc2} if file-based recovery + shards.assertAllEqual(initDocs + 1); } } diff --git a/server/src/test/java/org/elasticsearch/index/replication/RecoveryDuringReplicationTests.java b/server/src/test/java/org/elasticsearch/index/replication/RecoveryDuringReplicationTests.java index 28122665e9b..a73d7385d9d 100644 --- a/server/src/test/java/org/elasticsearch/index/replication/RecoveryDuringReplicationTests.java +++ b/server/src/test/java/org/elasticsearch/index/replication/RecoveryDuringReplicationTests.java @@ -55,10 +55,8 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; @@ -306,14 +304,6 @@ public class RecoveryDuringReplicationTests extends ESIndexLevelReplicationTestC assertThat(newReplica.recoveryState().getIndex().fileDetails(), not(empty())); assertThat(newReplica.recoveryState().getTranslog().recoveredOperations(), equalTo(uncommittedOpsOnPrimary)); } - - // roll back the extra ops in the replica - shards.removeReplica(replica); - replica.close("resync", false); - replica.store().close(); - newReplica = shards.addReplicaWithExistingPath(replica.shardPath(), replica.routingEntry().currentNodeId()); - shards.recoverReplica(newReplica); - shards.assertAllEqual(totalDocs); // Make sure that flushing on a recovering shard is ok. shards.flush(); shards.assertAllEqual(totalDocs); @@ -406,31 +396,14 @@ public class RecoveryDuringReplicationTests extends ESIndexLevelReplicationTestC indexOnReplica(bulkShardRequest, shards, justReplica); } - logger.info("--> seqNo primary {} replica {}", oldPrimary.seqNoStats(), newPrimary.seqNoStats()); - - logger.info("--> resyncing replicas"); + logger.info("--> resyncing replicas seqno_stats primary {} replica {}", oldPrimary.seqNoStats(), newPrimary.seqNoStats()); PrimaryReplicaSyncer.ResyncTask task = shards.promoteReplicaToPrimary(newPrimary).get(); if (syncedGlobalCheckPoint) { assertEquals(extraDocs, task.getResyncedOperations()); } else { assertThat(task.getResyncedOperations(), greaterThanOrEqualTo(extraDocs)); } - List replicas = shards.getReplicas(); - - // check all docs on primary are available on replica - Set primaryIds = getShardDocUIDs(newPrimary); - assertThat(primaryIds.size(), equalTo(initialDocs + extraDocs)); - for (IndexShard replica : replicas) { - Set replicaIds = getShardDocUIDs(replica); - Set temp = new HashSet<>(primaryIds); - temp.removeAll(replicaIds); - assertThat(replica.routingEntry() + " is missing docs", temp, empty()); - temp = new HashSet<>(replicaIds); - temp.removeAll(primaryIds); - // yeah, replica has more docs as there is no Lucene roll back on it - assertThat(replica.routingEntry() + " has to have extra docs", temp, - extraDocsToBeTrimmed > 0 ? not(empty()) : empty()); - } + shards.assertAllEqual(initialDocs + extraDocs); // check translog on replica is trimmed int translogOperations = 0; diff --git a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java index 7f37846d3f0..d6b80a6f6d7 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java @@ -106,6 +106,7 @@ import org.elasticsearch.index.store.Store; import org.elasticsearch.index.store.StoreStats; import org.elasticsearch.index.translog.TestTranslog; import org.elasticsearch.index.translog.Translog; +import org.elasticsearch.index.translog.TranslogStats; import org.elasticsearch.index.translog.TranslogTests; import org.elasticsearch.indices.IndicesQueryCache; import org.elasticsearch.indices.breaker.NoneCircuitBreakerService; @@ -181,6 +182,7 @@ import static org.hamcrest.Matchers.lessThanOrEqualTo; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.sameInstance; /** * Simple unit-test IndexShard related operations. @@ -945,28 +947,24 @@ public class IndexShardTests extends IndexShardTestCase { resyncLatch.await(); assertThat(indexShard.getLocalCheckpoint(), equalTo(maxSeqNo)); assertThat(indexShard.seqNoStats().getMaxSeqNo(), equalTo(maxSeqNo)); - - closeShards(indexShard); + closeShard(indexShard, false); } - public void testThrowBackLocalCheckpointOnReplica() throws IOException, InterruptedException { + public void testRollbackReplicaEngineOnPromotion() throws IOException, InterruptedException { final IndexShard indexShard = newStartedShard(false); // most of the time this is large enough that most of the time there will be at least one gap final int operations = 1024 - scaledRandomIntBetween(0, 1024); indexOnReplicaWithGaps(indexShard, operations, Math.toIntExact(SequenceNumbers.NO_OPS_PERFORMED)); - final long globalCheckpointOnReplica = - randomIntBetween( - Math.toIntExact(SequenceNumbers.UNASSIGNED_SEQ_NO), - Math.toIntExact(indexShard.getLocalCheckpoint())); + final long globalCheckpointOnReplica = randomLongBetween(SequenceNumbers.UNASSIGNED_SEQ_NO, indexShard.getLocalCheckpoint()); indexShard.updateGlobalCheckpointOnReplica(globalCheckpointOnReplica, "test"); - - final int globalCheckpoint = - randomIntBetween( - Math.toIntExact(SequenceNumbers.UNASSIGNED_SEQ_NO), - Math.toIntExact(indexShard.getLocalCheckpoint())); + final long globalCheckpoint = randomLongBetween(SequenceNumbers.UNASSIGNED_SEQ_NO, indexShard.getLocalCheckpoint()); + Set docsBelowGlobalCheckpoint = getShardDocUIDs(indexShard).stream() + .filter(id -> Long.parseLong(id) <= Math.max(globalCheckpointOnReplica, globalCheckpoint)).collect(Collectors.toSet()); final CountDownLatch latch = new CountDownLatch(1); + final boolean shouldRollback = Math.max(globalCheckpoint, globalCheckpointOnReplica) < indexShard.seqNoStats().getMaxSeqNo(); + final Engine beforeRollbackEngine = indexShard.getEngine(); indexShard.acquireReplicaOperationPermit( indexShard.pendingPrimaryTerm + 1, globalCheckpoint, @@ -985,18 +983,21 @@ public class IndexShardTests extends IndexShardTestCase { ThreadPool.Names.SAME, ""); latch.await(); - if (globalCheckpointOnReplica == SequenceNumbers.UNASSIGNED_SEQ_NO - && globalCheckpoint == SequenceNumbers.UNASSIGNED_SEQ_NO) { + if (globalCheckpointOnReplica == SequenceNumbers.UNASSIGNED_SEQ_NO && globalCheckpoint == SequenceNumbers.UNASSIGNED_SEQ_NO) { assertThat(indexShard.getLocalCheckpoint(), equalTo(SequenceNumbers.NO_OPS_PERFORMED)); } else { assertThat(indexShard.getLocalCheckpoint(), equalTo(Math.max(globalCheckpoint, globalCheckpointOnReplica))); } - + assertThat(getShardDocUIDs(indexShard), equalTo(docsBelowGlobalCheckpoint)); + if (shouldRollback) { + assertThat(indexShard.getEngine(), not(sameInstance(beforeRollbackEngine))); + } else { + assertThat(indexShard.getEngine(), sameInstance(beforeRollbackEngine)); + } // ensure that after the local checkpoint throw back and indexing again, the local checkpoint advances final Result result = indexOnReplicaWithGaps(indexShard, operations, Math.toIntExact(indexShard.getLocalCheckpoint())); assertThat(indexShard.getLocalCheckpoint(), equalTo((long) result.localCheckpoint)); - - closeShards(indexShard); + closeShard(indexShard, false); } public void testConcurrentTermIncreaseOnReplicaShard() throws BrokenBarrierException, InterruptedException, IOException { @@ -1880,13 +1881,17 @@ public class IndexShardTests extends IndexShardTestCase { SourceToParse.source(indexName, "_doc", "doc-1", new BytesArray("{}"), XContentType.JSON)); flushShard(shard); assertThat(getShardDocUIDs(shard), containsInAnyOrder("doc-0", "doc-1")); - // Simulate resync (without rollback): Noop #1, index #2 - acquireReplicaOperationPermitBlockingly(shard, shard.pendingPrimaryTerm + 1); + // Here we try to increase term (i.e. a new primary is promoted) without rolling back a replica so we can keep stale operations + // in the index commit; then verify that a recovery from store (started with the safe commit) will remove all stale operations. + shard.pendingPrimaryTerm++; + shard.operationPrimaryTerm++; + shard.getEngine().rollTranslogGeneration(); shard.markSeqNoAsNoop(1, "test"); shard.applyIndexOperationOnReplica(2, 1, IndexRequest.UNSET_AUTO_GENERATED_TIMESTAMP, false, SourceToParse.source(indexName, "_doc", "doc-2", new BytesArray("{}"), XContentType.JSON)); flushShard(shard); assertThat(getShardDocUIDs(shard), containsInAnyOrder("doc-0", "doc-1", "doc-2")); + closeShard(shard, false); // Recovering from store should discard doc #1 final ShardRouting replicaRouting = shard.routingEntry(); IndexShard newShard = reinitShard(shard, @@ -2249,10 +2254,11 @@ public class IndexShardTests extends IndexShardTestCase { null)); primary.recoverFromStore(); + primary.recoveryState().getTranslog().totalOperations(snapshot.totalOperations()); + primary.recoveryState().getTranslog().totalOperationsOnStart(snapshot.totalOperations()); primary.state = IndexShardState.RECOVERING; // translog recovery on the next line would otherwise fail as we are in POST_RECOVERY - primary.runTranslogRecovery(primary.getEngine(), snapshot); - assertThat(primary.recoveryState().getTranslog().totalOperationsOnStart(), equalTo(numTotalEntries)); - assertThat(primary.recoveryState().getTranslog().totalOperations(), equalTo(numTotalEntries)); + primary.runTranslogRecovery(primary.getEngine(), snapshot, Engine.Operation.Origin.LOCAL_TRANSLOG_RECOVERY, + primary.recoveryState().getTranslog()::incrementRecoveredOperations); assertThat(primary.recoveryState().getTranslog().recoveredOperations(), equalTo(numTotalEntries - numCorruptEntries)); closeShards(primary); @@ -2865,6 +2871,9 @@ public class IndexShardTests extends IndexShardTestCase { } else { gap = true; } + if (rarely()) { + indexShard.flush(new FlushRequest()); + } } assert localCheckpoint == indexShard.getLocalCheckpoint(); assert !gap || (localCheckpoint != max); @@ -3402,4 +3411,19 @@ public class IndexShardTests extends IndexShardTestCase { closeShards(shard); } + + public void testResetEngine() throws Exception { + IndexShard shard = newStartedShard(false); + indexOnReplicaWithGaps(shard, between(0, 1000), Math.toIntExact(shard.getLocalCheckpoint())); + final long globalCheckpoint = randomLongBetween(shard.getGlobalCheckpoint(), shard.getLocalCheckpoint()); + shard.updateGlobalCheckpointOnReplica(globalCheckpoint, "test"); + Set docBelowGlobalCheckpoint = getShardDocUIDs(shard).stream() + .filter(id -> Long.parseLong(id) <= globalCheckpoint).collect(Collectors.toSet()); + TranslogStats translogStats = shard.translogStats(); + shard.resetEngineToGlobalCheckpoint(); + assertThat(getShardDocUIDs(shard), equalTo(docBelowGlobalCheckpoint)); + assertThat(shard.seqNoStats().getMaxSeqNo(), equalTo(globalCheckpoint)); + assertThat(shard.translogStats().estimatedNumberOfOperations(), equalTo(translogStats.estimatedNumberOfOperations())); + closeShard(shard, false); + } } diff --git a/server/src/test/java/org/elasticsearch/recovery/RelocationIT.java b/server/src/test/java/org/elasticsearch/recovery/RelocationIT.java index cb93d803bb7..8d0f1845be6 100644 --- a/server/src/test/java/org/elasticsearch/recovery/RelocationIT.java +++ b/server/src/test/java/org/elasticsearch/recovery/RelocationIT.java @@ -103,6 +103,7 @@ public class RelocationIT extends ESIntegTestCase { protected void beforeIndexDeletion() throws Exception { super.beforeIndexDeletion(); assertSeqNos(); + assertSameDocIdsOnShards(); } public void testSimpleRelocationNoIndexing() { diff --git a/test/framework/src/main/java/org/elasticsearch/index/engine/DocIdSeqNoAndTerm.java b/test/framework/src/main/java/org/elasticsearch/index/engine/DocIdSeqNoAndTerm.java new file mode 100644 index 00000000000..b24a010c1a0 --- /dev/null +++ b/test/framework/src/main/java/org/elasticsearch/index/engine/DocIdSeqNoAndTerm.java @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.index.engine; + + +import java.util.Objects; + +/** A tuple of document id, sequence number and primary term of a document */ +public final class DocIdSeqNoAndTerm { + private final String id; + private final long seqNo; + private final long primaryTerm; + + public DocIdSeqNoAndTerm(String id, long seqNo, long primaryTerm) { + this.id = id; + this.seqNo = seqNo; + this.primaryTerm = primaryTerm; + } + + public String getId() { + return id; + } + + public long getSeqNo() { + return seqNo; + } + + public long getPrimaryTerm() { + return primaryTerm; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DocIdSeqNoAndTerm that = (DocIdSeqNoAndTerm) o; + return Objects.equals(id, that.id) && seqNo == that.seqNo && primaryTerm == that.primaryTerm; + } + + @Override + public int hashCode() { + return Objects.hash(id, seqNo, primaryTerm); + } + + @Override + public String toString() { + return "DocIdSeqNoAndTerm{" + "id='" + id + " seqNo=" + seqNo + " primaryTerm=" + primaryTerm + "}"; + } +} diff --git a/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java b/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java index 283a7b13753..f9377afe6ed 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java @@ -33,6 +33,7 @@ import org.apache.lucene.index.LeafReader; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.index.LiveIndexWriterConfig; import org.apache.lucene.index.MergePolicy; +import org.apache.lucene.index.NumericDocValues; import org.apache.lucene.index.Term; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.MatchAllDocsQuery; @@ -95,11 +96,10 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -775,26 +775,41 @@ public abstract class EngineTestCase extends ESTestCase { } /** - * Gets all docId from the given engine. + * Gets a collection of tuples of docId, sequence number, and primary term of all live documents in the provided engine. */ - public static Set getDocIds(Engine engine, boolean refresh) throws IOException { + public static List getDocIds(Engine engine, boolean refresh) throws IOException { if (refresh) { engine.refresh("test_get_doc_ids"); } try (Engine.Searcher searcher = engine.acquireSearcher("test_get_doc_ids")) { - Set ids = new HashSet<>(); + List docs = new ArrayList<>(); for (LeafReaderContext leafContext : searcher.reader().leaves()) { LeafReader reader = leafContext.reader(); + NumericDocValues seqNoDocValues = reader.getNumericDocValues(SeqNoFieldMapper.NAME); + NumericDocValues primaryTermDocValues = reader.getNumericDocValues(SeqNoFieldMapper.PRIMARY_TERM_NAME); Bits liveDocs = reader.getLiveDocs(); for (int i = 0; i < reader.maxDoc(); i++) { if (liveDocs == null || liveDocs.get(i)) { Document uuid = reader.document(i, Collections.singleton(IdFieldMapper.NAME)); BytesRef binaryID = uuid.getBinaryValue(IdFieldMapper.NAME); - ids.add(Uid.decodeId(Arrays.copyOfRange(binaryID.bytes, binaryID.offset, binaryID.offset + binaryID.length))); + String id = Uid.decodeId(Arrays.copyOfRange(binaryID.bytes, binaryID.offset, binaryID.offset + binaryID.length)); + final long primaryTerm; + if (primaryTermDocValues.advanceExact(i)) { + primaryTerm = primaryTermDocValues.longValue(); + } else { + primaryTerm = 0; // non-root documents of a nested document. + } + if (seqNoDocValues.advanceExact(i) == false) { + throw new AssertionError("seqNoDocValues not found for doc[" + i + "] id[" + id + "]"); + } + final long seqNo = seqNoDocValues.longValue(); + docs.add(new DocIdSeqNoAndTerm(id, seqNo, primaryTerm)); } } } - return ids; + docs.sort(Comparator.comparing(DocIdSeqNoAndTerm::getId) + .thenComparingLong(DocIdSeqNoAndTerm::getSeqNo).thenComparingLong(DocIdSeqNoAndTerm::getPrimaryTerm)); + return docs; } } diff --git a/test/framework/src/main/java/org/elasticsearch/index/shard/IndexShardTestCase.java b/test/framework/src/main/java/org/elasticsearch/index/shard/IndexShardTestCase.java index ca2156144b3..a9e715a1129 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/shard/IndexShardTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/index/shard/IndexShardTestCase.java @@ -49,6 +49,7 @@ import org.elasticsearch.index.MapperTestUtils; import org.elasticsearch.index.VersionType; import org.elasticsearch.index.cache.IndexCache; import org.elasticsearch.index.cache.query.DisabledQueryCache; +import org.elasticsearch.index.engine.DocIdSeqNoAndTerm; import org.elasticsearch.index.engine.Engine; import org.elasticsearch.index.engine.EngineFactory; import org.elasticsearch.index.engine.EngineTestCase; @@ -82,12 +83,14 @@ import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiFunction; import java.util.function.Consumer; +import java.util.stream.Collectors; import static org.elasticsearch.cluster.routing.TestShardRouting.newShardRouting; import static org.hamcrest.Matchers.contains; @@ -451,15 +454,20 @@ public abstract class IndexShardTestCase extends ESTestCase { closeShards(Arrays.asList(shards)); } + protected void closeShard(IndexShard shard, boolean assertConsistencyBetweenTranslogAndLucene) throws IOException { + try { + if (assertConsistencyBetweenTranslogAndLucene) { + assertConsistentHistoryBetweenTranslogAndLucene(shard); + } + } finally { + IOUtils.close(() -> shard.close("test", false), shard.store()); + } + } + protected void closeShards(Iterable shards) throws IOException { for (IndexShard shard : shards) { if (shard != null) { - try { - assertConsistentHistoryBetweenTranslogAndLucene(shard); - shard.close("test", false); - } finally { - IOUtils.close(shard.store()); - } + closeShard(shard, true); } } } @@ -635,7 +643,11 @@ public abstract class IndexShardTestCase extends ESTestCase { return result; } - protected Set getShardDocUIDs(final IndexShard shard) throws IOException { + public static Set getShardDocUIDs(final IndexShard shard) throws IOException { + return getDocIdAndSeqNos(shard).stream().map(DocIdSeqNoAndTerm::getId).collect(Collectors.toSet()); + } + + public static List getDocIdAndSeqNos(final IndexShard shard) throws IOException { return EngineTestCase.getDocIds(shard.getEngine(), true); } diff --git a/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java index 68a862c109d..52ed2205ab5 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java @@ -125,6 +125,7 @@ import org.elasticsearch.index.MergePolicyConfig; import org.elasticsearch.index.MergeSchedulerConfig; import org.elasticsearch.index.MockEngineFactoryPlugin; import org.elasticsearch.index.codec.CodecService; +import org.elasticsearch.index.engine.DocIdSeqNoAndTerm; import org.elasticsearch.index.engine.Segment; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.MapperService; @@ -132,6 +133,7 @@ import org.elasticsearch.index.mapper.MockFieldFilterPlugin; import org.elasticsearch.index.seqno.SeqNoStats; import org.elasticsearch.index.seqno.SequenceNumbers; import org.elasticsearch.index.shard.IndexShard; +import org.elasticsearch.index.shard.IndexShardTestCase; import org.elasticsearch.index.translog.Translog; import org.elasticsearch.indices.IndicesQueryCache; import org.elasticsearch.indices.IndicesRequestCache; @@ -2380,6 +2382,49 @@ public abstract class ESIntegTestCase extends ESTestCase { }); } + /** + * Asserts that all shards with the same shardId should have document Ids. + */ + public void assertSameDocIdsOnShards() throws Exception { + assertBusy(() -> { + ClusterState state = client().admin().cluster().prepareState().get().getState(); + for (ObjectObjectCursor indexRoutingTable : state.routingTable().indicesRouting()) { + for (IntObjectCursor indexShardRoutingTable : indexRoutingTable.value.shards()) { + ShardRouting primaryShardRouting = indexShardRoutingTable.value.primaryShard(); + if (primaryShardRouting == null || primaryShardRouting.assignedToNode() == false) { + continue; + } + DiscoveryNode primaryNode = state.nodes().get(primaryShardRouting.currentNodeId()); + IndexShard primaryShard = internalCluster().getInstance(IndicesService.class, primaryNode.getName()) + .indexServiceSafe(primaryShardRouting.index()).getShard(primaryShardRouting.id()); + final List docsOnPrimary; + try { + docsOnPrimary = IndexShardTestCase.getDocIdAndSeqNos(primaryShard); + } catch (AlreadyClosedException ex) { + continue; + } + for (ShardRouting replicaShardRouting : indexShardRoutingTable.value.replicaShards()) { + if (replicaShardRouting.assignedToNode() == false) { + continue; + } + DiscoveryNode replicaNode = state.nodes().get(replicaShardRouting.currentNodeId()); + IndexShard replicaShard = internalCluster().getInstance(IndicesService.class, replicaNode.getName()) + .indexServiceSafe(replicaShardRouting.index()).getShard(replicaShardRouting.id()); + final List docsOnReplica; + try { + docsOnReplica = IndexShardTestCase.getDocIdAndSeqNos(replicaShard); + } catch (AlreadyClosedException ex) { + continue; + } + assertThat("out of sync shards: primary=[" + primaryShardRouting + "] num_docs_on_primary=[" + docsOnPrimary.size() + + "] vs replica=[" + replicaShardRouting + "] num_docs_on_replica=[" + docsOnReplica.size() + "]", + docsOnReplica, equalTo(docsOnPrimary)); + } + } + } + }); + } + public static boolean inFipsJvm() { return Security.getProviders()[0].getName().toLowerCase(Locale.ROOT).contains("fips"); } From 9752540866ee9fee3946b2b0c7a0021855670ab4 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Tue, 11 Sep 2018 23:17:26 -0400 Subject: [PATCH 18/78] Add test coverage for global checkpoint listeners This commit adds test coverage for two cases not previously covered by the existing testing. Namely, we add coverage ensuring that the executor is used to notify listeners being added that are immediately notified because the shard is closed or because the global checkpoint is already beyond what the listener knows. --- .../shard/GlobalCheckpointListeners.java | 1 - .../shard/GlobalCheckpointListenersTests.java | 55 ++++++++++++++++++- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java b/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java index e279badec4a..825a8a8a483 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java +++ b/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java @@ -97,7 +97,6 @@ public class GlobalCheckpointListeners implements Closeable { if (lastKnownGlobalCheckpoint > currentGlobalCheckpoint) { // notify directly executor.execute(() -> notifyListener(listener, lastKnownGlobalCheckpoint, null)); - return; } else { if (listeners == null) { listeners = new ArrayList<>(); diff --git a/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java b/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java index 43b16c6ecc7..bb3a691a702 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java @@ -336,14 +336,65 @@ public class GlobalCheckpointListenersTests extends ESTestCase { }; final GlobalCheckpointListeners globalCheckpointListeners = new GlobalCheckpointListeners(shardId, executor, logger); globalCheckpointListeners.globalCheckpointUpdated(NO_OPS_PERFORMED); + final long globalCheckpoint = randomLongBetween(NO_OPS_PERFORMED, Long.MAX_VALUE); + final AtomicInteger notified = new AtomicInteger(); final int numberOfListeners = randomIntBetween(0, 16); for (int i = 0; i < numberOfListeners; i++) { - globalCheckpointListeners.add(NO_OPS_PERFORMED, (g, e) -> {}); + globalCheckpointListeners.add(NO_OPS_PERFORMED, (g, e) -> { + notified.incrementAndGet(); + assertThat(g, equalTo(globalCheckpoint)); + assertNull(e); + }); } - globalCheckpointListeners.globalCheckpointUpdated(randomLongBetween(NO_OPS_PERFORMED, Long.MAX_VALUE)); + globalCheckpointListeners.globalCheckpointUpdated(globalCheckpoint); + assertThat(notified.get(), equalTo(numberOfListeners)); assertThat(count.get(), equalTo(numberOfListeners == 0 ? 0 : 1)); } + public void testNotificationOnClosedUsesExecutor() throws IOException { + final AtomicInteger count = new AtomicInteger(); + final Executor executor = command -> { + count.incrementAndGet(); + command.run(); + }; + final GlobalCheckpointListeners globalCheckpointListeners = new GlobalCheckpointListeners(shardId, executor, logger); + globalCheckpointListeners.close(); + final AtomicInteger notified = new AtomicInteger(); + final int numberOfListeners = randomIntBetween(0, 16); + for (int i = 0; i < numberOfListeners; i++) { + globalCheckpointListeners.add(NO_OPS_PERFORMED, (g, e) -> { + notified.incrementAndGet(); + assertThat(g, equalTo(UNASSIGNED_SEQ_NO)); + assertNotNull(e); + assertThat(e.getShardId(), equalTo(shardId)); + }); + } + assertThat(notified.get(), equalTo(numberOfListeners)); + assertThat(count.get(), equalTo(numberOfListeners)); + } + + public void testListenersReadyToBeNotifiedUsesExecutor() { + final AtomicInteger count = new AtomicInteger(); + final Executor executor = command -> { + count.incrementAndGet(); + command.run(); + }; + final GlobalCheckpointListeners globalCheckpointListeners = new GlobalCheckpointListeners(shardId, executor, logger); + final long globalCheckpoint = randomNonNegativeLong(); + globalCheckpointListeners.globalCheckpointUpdated(globalCheckpoint); + final AtomicInteger notified = new AtomicInteger(); + final int numberOfListeners = randomIntBetween(0, 16); + for (int i = 0; i < numberOfListeners; i++) { + globalCheckpointListeners.add(randomLongBetween(0, globalCheckpoint), (g, e) -> { + notified.incrementAndGet(); + assertThat(g, equalTo(globalCheckpoint)); + assertNull(e); + }); + } + assertThat(notified.get(), equalTo(numberOfListeners)); + assertThat(count.get(), equalTo(numberOfListeners)); + } + public void testConcurrency() throws BrokenBarrierException, InterruptedException { final ExecutorService executor = Executors.newFixedThreadPool(randomIntBetween(1, 8)); final GlobalCheckpointListeners globalCheckpointListeners = new GlobalCheckpointListeners(shardId, executor, logger); From 94cdf0cebacde4188e6ca5f84843d732aae8cf7c Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Wed, 12 Sep 2018 06:15:36 +0200 Subject: [PATCH 19/78] NETWORKING: http.publish_host Should Contain CNAME (#32806) * NETWORKING: http.publish_host Should Contain CNAME * Closes #22029 --- .../elasticsearch/gradle/BuildPlugin.groovy | 3 + docs/build.gradle | 2 + modules/lang-painless/build.gradle | 1 + .../java/org/elasticsearch/http/HttpInfo.java | 47 +++++++-- .../org/elasticsearch/http/HttpInfoTests.java | 97 +++++++++++++++++++ 5 files changed, 142 insertions(+), 8 deletions(-) create mode 100644 server/src/test/java/org/elasticsearch/http/HttpInfoTests.java diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy index 110982e31e6..05e07049695 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy @@ -831,6 +831,9 @@ class BuildPlugin implements Plugin { // TODO: remove this once ctx isn't added to update script params in 7.0 systemProperty 'es.scripting.update.ctx_in_params', 'false' + //TODO: remove this once the cname is prepended to the address by default in 7.0 + systemProperty 'es.http.cname_in_publish_address', 'true' + // Set the system keystore/truststore password if we're running tests in a FIPS-140 JVM if (project.inFipsJvm) { systemProperty 'javax.net.ssl.trustStorePassword', 'password' diff --git a/docs/build.gradle b/docs/build.gradle index c6a7a8d4837..f2a7f8511e3 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -57,6 +57,8 @@ integTestCluster { // TODO: remove this for 7.0, this exists to allow the doc examples in 6.x to continue using the defaults systemProperty 'es.scripting.use_java_time', 'false' systemProperty 'es.scripting.update.ctx_in_params', 'false' + //TODO: remove this once the cname is prepended to the address by default in 7.0 + systemProperty 'es.http.cname_in_publish_address', 'true' } // remove when https://github.com/elastic/elasticsearch/issues/31305 is fixed diff --git a/modules/lang-painless/build.gradle b/modules/lang-painless/build.gradle index ed4b1d631e0..6bec6f50626 100644 --- a/modules/lang-painless/build.gradle +++ b/modules/lang-painless/build.gradle @@ -26,6 +26,7 @@ integTestCluster { module project.project(':modules:mapper-extras') systemProperty 'es.scripting.use_java_time', 'true' systemProperty 'es.scripting.update.ctx_in_params', 'false' + systemProperty 'es.http.cname_in_publish_address', 'true' } dependencies { diff --git a/server/src/main/java/org/elasticsearch/http/HttpInfo.java b/server/src/main/java/org/elasticsearch/http/HttpInfo.java index 4e944a0f7fa..aece8131994 100644 --- a/server/src/main/java/org/elasticsearch/http/HttpInfo.java +++ b/server/src/main/java/org/elasticsearch/http/HttpInfo.java @@ -19,24 +19,46 @@ package org.elasticsearch.http; +import org.apache.logging.log4j.LogManager; 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.logging.DeprecationLogger; +import org.elasticsearch.common.network.InetAddresses; import org.elasticsearch.common.transport.BoundTransportAddress; +import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.xcontent.ToXContentFragment; import org.elasticsearch.common.xcontent.XContentBuilder; import java.io.IOException; +import static org.elasticsearch.common.Booleans.parseBoolean; + public class HttpInfo implements Writeable, ToXContentFragment { + private static final DeprecationLogger DEPRECATION_LOGGER = new DeprecationLogger(LogManager.getLogger(HttpInfo.class)); + + /** Whether to add hostname to publish host field when serializing. */ + private static final boolean CNAME_IN_PUBLISH_HOST = + parseBoolean(System.getProperty("es.http.cname_in_publish_address"), false); + private final BoundTransportAddress address; private final long maxContentLength; + private final boolean cnameInPublishHost; public HttpInfo(StreamInput in) throws IOException { - address = BoundTransportAddress.readBoundTransportAddress(in); - maxContentLength = in.readLong(); + this(BoundTransportAddress.readBoundTransportAddress(in), in.readLong(), CNAME_IN_PUBLISH_HOST); + } + + public HttpInfo(BoundTransportAddress address, long maxContentLength) { + this(address, maxContentLength, CNAME_IN_PUBLISH_HOST); + } + + HttpInfo(BoundTransportAddress address, long maxContentLength, boolean cnameInPublishHost) { + this.address = address; + this.maxContentLength = maxContentLength; + this.cnameInPublishHost = cnameInPublishHost; } @Override @@ -45,11 +67,6 @@ public class HttpInfo implements Writeable, ToXContentFragment { out.writeLong(maxContentLength); } - public HttpInfo(BoundTransportAddress address, long maxContentLength) { - this.address = address; - this.maxContentLength = maxContentLength; - } - static final class Fields { static final String HTTP = "http"; static final String BOUND_ADDRESS = "bound_address"; @@ -62,7 +79,21 @@ public class HttpInfo implements Writeable, ToXContentFragment { public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(Fields.HTTP); builder.array(Fields.BOUND_ADDRESS, (Object[]) address.boundAddresses()); - builder.field(Fields.PUBLISH_ADDRESS, address.publishAddress().toString()); + TransportAddress publishAddress = address.publishAddress(); + String publishAddressString = publishAddress.toString(); + String hostString = publishAddress.address().getHostString(); + if (InetAddresses.isInetAddress(hostString) == false) { + if (cnameInPublishHost) { + publishAddressString = hostString + '/' + publishAddress.toString(); + } else { + DEPRECATION_LOGGER.deprecated( + "[http.publish_host] was printed as [ip:port] instead of [hostname/ip:port]. " + + "This format is deprecated and will change to [hostname/ip:port] in a future version. " + + "Use -Des.http.cname_in_publish_address=true to enforce non-deprecated formatting." + ); + } + } + builder.field(Fields.PUBLISH_ADDRESS, publishAddressString); builder.humanReadableField(Fields.MAX_CONTENT_LENGTH_IN_BYTES, Fields.MAX_CONTENT_LENGTH, maxContentLength()); builder.endObject(); return builder; diff --git a/server/src/test/java/org/elasticsearch/http/HttpInfoTests.java b/server/src/test/java/org/elasticsearch/http/HttpInfoTests.java new file mode 100644 index 00000000000..db149bd6d0d --- /dev/null +++ b/server/src/test/java/org/elasticsearch/http/HttpInfoTests.java @@ -0,0 +1,97 @@ +/* + * 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.http; + +import java.io.IOException; +import java.net.InetAddress; +import java.util.Map; +import org.elasticsearch.common.network.NetworkAddress; +import org.elasticsearch.common.transport.BoundTransportAddress; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.test.ESTestCase; + +public class HttpInfoTests extends ESTestCase { + + public void testCorrectlyDisplayPublishedCname() throws Exception { + InetAddress localhost = InetAddress.getByName("localhost"); + int port = 9200; + assertPublishAddress( + new HttpInfo( + new BoundTransportAddress( + new TransportAddress[]{new TransportAddress(localhost, port)}, + new TransportAddress(localhost, port) + ), 0L, true + ), "localhost/" + NetworkAddress.format(localhost) + ':' + port + ); + } + + public void hideCnameIfDeprecatedFormat() throws Exception { + InetAddress localhost = InetAddress.getByName("localhost"); + int port = 9200; + assertPublishAddress( + new HttpInfo( + new BoundTransportAddress( + new TransportAddress[]{new TransportAddress(localhost, port)}, + new TransportAddress(localhost, port) + ), 0L, false + ), NetworkAddress.format(localhost) + ':' + port + ); + } + + public void testCorrectDisplayPublishedIp() throws Exception { + InetAddress localhost = InetAddress.getByName(NetworkAddress.format(InetAddress.getByName("localhost"))); + int port = 9200; + assertPublishAddress( + new HttpInfo( + new BoundTransportAddress( + new TransportAddress[]{new TransportAddress(localhost, port)}, + new TransportAddress(localhost, port) + ), 0L, true + ), NetworkAddress.format(localhost) + ':' + port + ); + } + + public void testCorrectDisplayPublishedIpv6() throws Exception { + int port = 9200; + TransportAddress localhost = + new TransportAddress(InetAddress.getByName(NetworkAddress.format(InetAddress.getByName("0:0:0:0:0:0:0:1"))), port); + assertPublishAddress( + new HttpInfo( + new BoundTransportAddress(new TransportAddress[]{localhost}, localhost), 0L, true + ), localhost.toString() + ); + } + + @SuppressWarnings("unchecked") + private void assertPublishAddress(HttpInfo httpInfo, String expected) throws IOException { + XContentBuilder builder = XContentFactory.jsonBuilder(); + builder.startObject(); + httpInfo.toXContent(builder, ToXContent.EMPTY_PARAMS); + builder.endObject(); + assertEquals( + expected, + ((Map) createParser(builder).map().get(HttpInfo.Fields.HTTP)) + .get(HttpInfo.Fields.PUBLISH_ADDRESS) + ); + } +} From c74c46edc3c583d09e88ac5d56352fdccdedf5ab Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Wed, 12 Sep 2018 01:14:43 -0400 Subject: [PATCH 20/78] Upgrade remote cluster settings (#33537) This commit adds settings upgraders for the search.remote.* settings that can be in the cluster state to automatically upgrade these settings to cluster.remote.*. Because of the infrastructure that we have here, these settings can be upgraded when recovering the cluster state, but also when a user tries to make a dynamic update for these settings. --- .../upgrades/FullClusterRestartIT.java | 12 +-- .../FullClusterRestartSettingsUpgradeIT.java | 101 ++++++++++++++++++ .../settings/AbstractScopedSettings.java | 3 +- .../common/settings/ClusterSettings.java | 5 +- .../transport/RemoteClusterAware.java | 29 +++++ .../transport/RemoteClusterService.java | 19 +++- .../common/settings/UpgradeSettingsIT.java | 34 ++++++ x-pack/qa/full-cluster-restart/build.gradle | 2 + .../FullClusterRestartSettingsUpgradeIT.java | 24 +++++ 9 files changed, 216 insertions(+), 13 deletions(-) create mode 100644 qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartSettingsUpgradeIT.java create mode 100644 x-pack/qa/full-cluster-restart/src/test/java/org/elasticsearch/xpack/restart/FullClusterRestartSettingsUpgradeIT.java diff --git a/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartIT.java b/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartIT.java index 3ee5c07308f..7efebd1d54a 100644 --- a/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartIT.java +++ b/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartIT.java @@ -997,15 +997,9 @@ public class FullClusterRestartIT extends AbstractFullClusterRestartTestCase { Request clusterSettingsRequest = new Request("GET", "/_cluster/settings"); clusterSettingsRequest.addParameter("flat_settings", "true"); Map clusterSettingsResponse = entityAsMap(client().performRequest(clusterSettingsRequest)); - Map expectedClusterSettings = new HashMap<>(); - expectedClusterSettings.put("transient", emptyMap()); - expectedClusterSettings.put("persistent", - singletonMap("cluster.routing.allocation.exclude.test_attr", getOldClusterVersion().toString())); - if (expectedClusterSettings.equals(clusterSettingsResponse) == false) { - NotEqualMessageBuilder builder = new NotEqualMessageBuilder(); - builder.compareMaps(clusterSettingsResponse, expectedClusterSettings); - fail("settings don't match:\n" + builder.toString()); - } + @SuppressWarnings("unchecked") final Map persistentSettings = + (Map)clusterSettingsResponse.get("persistent"); + assertThat(persistentSettings.get("cluster.routing.allocation.exclude.test_attr"), equalTo(getOldClusterVersion().toString())); // Check that the template was restored successfully Map getTemplateResponse = entityAsMap(client().performRequest(new Request("GET", "/_template/test_template"))); diff --git a/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartSettingsUpgradeIT.java b/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartSettingsUpgradeIT.java new file mode 100644 index 00000000000..8cb1ca717a8 --- /dev/null +++ b/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartSettingsUpgradeIT.java @@ -0,0 +1,101 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.upgrades; + +import org.elasticsearch.Version; +import org.elasticsearch.action.admin.cluster.settings.ClusterGetSettingsResponse; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Response; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.transport.RemoteClusterService; + +import java.io.IOException; +import java.util.Collections; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.elasticsearch.transport.RemoteClusterAware.SEARCH_REMOTE_CLUSTERS_SEEDS; +import static org.elasticsearch.transport.RemoteClusterService.SEARCH_REMOTE_CLUSTER_SKIP_UNAVAILABLE; +import static org.hamcrest.Matchers.equalTo; + +public class FullClusterRestartSettingsUpgradeIT extends AbstractFullClusterRestartTestCase { + + public void testRemoteClusterSettingsUpgraded() throws IOException { + assumeTrue("settings automatically upgraded since 7.0.0", getOldClusterVersion().before(Version.V_7_0_0_alpha1)); + if (isRunningAgainstOldCluster()) { + final Request putSettingsRequest = new Request("PUT", "/_cluster/settings"); + try (XContentBuilder builder = jsonBuilder()) { + builder.startObject(); + { + builder.startObject("persistent"); + { + builder.field("search.remote.foo.skip_unavailable", true); + builder.field("search.remote.foo.seeds", Collections.singletonList("localhost:9200")); + } + builder.endObject(); + } + builder.endObject(); + putSettingsRequest.setJsonEntity(Strings.toString(builder)); + } + client().performRequest(putSettingsRequest); + + final Request getSettingsRequest = new Request("GET", "/_cluster/settings"); + final Response response = client().performRequest(getSettingsRequest); + try (XContentParser parser = createParser(JsonXContent.jsonXContent, response.getEntity().getContent())) { + final ClusterGetSettingsResponse clusterGetSettingsResponse = ClusterGetSettingsResponse.fromXContent(parser); + final Settings settings = clusterGetSettingsResponse.getPersistentSettings(); + + assertTrue(SEARCH_REMOTE_CLUSTER_SKIP_UNAVAILABLE.getConcreteSettingForNamespace("foo").exists(settings)); + assertTrue(SEARCH_REMOTE_CLUSTER_SKIP_UNAVAILABLE.getConcreteSettingForNamespace("foo").get(settings)); + assertTrue(SEARCH_REMOTE_CLUSTERS_SEEDS.getConcreteSettingForNamespace("foo").exists(settings)); + assertThat( + SEARCH_REMOTE_CLUSTERS_SEEDS.getConcreteSettingForNamespace("foo").get(settings), + equalTo(Collections.singletonList("localhost:9200"))); + } + + assertSettingDeprecationsAndWarnings(new Setting[]{ + SEARCH_REMOTE_CLUSTER_SKIP_UNAVAILABLE.getConcreteSettingForNamespace("foo"), + SEARCH_REMOTE_CLUSTERS_SEEDS.getConcreteSettingForNamespace("foo")}); + } else { + final Request getSettingsRequest = new Request("GET", "/_cluster/settings"); + final Response getSettingsResponse = client().performRequest(getSettingsRequest); + try (XContentParser parser = createParser(JsonXContent.jsonXContent, getSettingsResponse.getEntity().getContent())) { + final ClusterGetSettingsResponse clusterGetSettingsResponse = ClusterGetSettingsResponse.fromXContent(parser); + final Settings settings = clusterGetSettingsResponse.getPersistentSettings(); + + assertFalse(SEARCH_REMOTE_CLUSTER_SKIP_UNAVAILABLE.getConcreteSettingForNamespace("foo").exists(settings)); + assertTrue( + settings.toString(), + RemoteClusterService.REMOTE_CLUSTER_SKIP_UNAVAILABLE.getConcreteSettingForNamespace("foo").exists(settings)); + assertTrue(RemoteClusterService.REMOTE_CLUSTER_SKIP_UNAVAILABLE.getConcreteSettingForNamespace("foo").get(settings)); + assertFalse(SEARCH_REMOTE_CLUSTERS_SEEDS.getConcreteSettingForNamespace("foo").exists(settings)); + assertTrue(RemoteClusterService.REMOTE_CLUSTERS_SEEDS.getConcreteSettingForNamespace("foo").exists(settings)); + assertThat( + RemoteClusterService.REMOTE_CLUSTERS_SEEDS.getConcreteSettingForNamespace("foo").get(settings), + equalTo(Collections.singletonList("localhost:9200"))); + } + } + } + +} diff --git a/server/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java b/server/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java index b010d7982fd..e87b3757e6b 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java @@ -788,7 +788,8 @@ public abstract class AbstractScopedSettings extends AbstractComponent { } else { // the setting has an upgrader, so mark that we have changed a setting and apply the upgrade logic changed = true; - if (setting.isListSetting()) { + // noinspection ConstantConditions + if (setting.getConcreteSetting(key).isListSetting()) { final List value = settings.getAsList(key); final String upgradedKey = upgrader.getKey(key); final List upgradedValue = upgrader.getListValue(value); diff --git a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java index cb369d6cfda..7e90aa3f442 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java @@ -443,6 +443,9 @@ public final class ClusterSettings extends AbstractScopedSettings { EnableAssignmentDecider.CLUSTER_TASKS_ALLOCATION_ENABLE_SETTING ))); - public static List> BUILT_IN_SETTING_UPGRADERS = Collections.emptyList(); + public static List> BUILT_IN_SETTING_UPGRADERS = Collections.unmodifiableList(Arrays.asList( + RemoteClusterAware.SEARCH_REMOTE_CLUSTER_SEEDS_UPGRADER, + RemoteClusterAware.SEARCH_REMOTE_CLUSTERS_PROXY_UPGRADER, + RemoteClusterService.SEARCH_REMOTE_CLUSTER_SKIP_UNAVAILABLE_UPGRADER)); } diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteClusterAware.java b/server/src/main/java/org/elasticsearch/transport/RemoteClusterAware.java index f08ef75612f..1c87af4a442 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteClusterAware.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteClusterAware.java @@ -28,6 +28,7 @@ import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.component.AbstractComponent; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.SettingUpgrader; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.TransportAddress; @@ -66,6 +67,20 @@ public abstract class RemoteClusterAware extends AbstractComponent { Setting.Property.Dynamic, Setting.Property.NodeScope)); + public static final SettingUpgrader> SEARCH_REMOTE_CLUSTER_SEEDS_UPGRADER = new SettingUpgrader>() { + + @Override + public Setting> getSetting() { + return SEARCH_REMOTE_CLUSTERS_SEEDS; + } + + @Override + public String getKey(final String key) { + return key.replaceFirst("^search", "cluster"); + } + + }; + /** * A list of initial seed nodes to discover eligible nodes from the remote cluster */ @@ -105,6 +120,20 @@ public abstract class RemoteClusterAware extends AbstractComponent { Setting.Property.NodeScope), REMOTE_CLUSTERS_SEEDS); + public static final SettingUpgrader SEARCH_REMOTE_CLUSTERS_PROXY_UPGRADER = new SettingUpgrader() { + + @Override + public Setting getSetting() { + return SEARCH_REMOTE_CLUSTERS_PROXY; + } + + @Override + public String getKey(final String key) { + return key.replaceFirst("^search", "cluster"); + } + + }; + /** * A proxy address for the remote cluster. * NOTE: this settings is undocumented until we have at last one transport that supports passing diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java b/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java index 0e8bd5cb28d..04cb1ab3e56 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java @@ -19,8 +19,6 @@ package org.elasticsearch.transport; -import java.util.Collection; -import java.util.function.Supplier; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.OriginalIndices; @@ -35,6 +33,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.SettingUpgrader; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.CountDown; @@ -43,6 +42,7 @@ import org.elasticsearch.threadpool.ThreadPool; import java.io.Closeable; import java.io.IOException; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -55,6 +55,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -132,6 +133,20 @@ public final class RemoteClusterService extends RemoteClusterAware implements Cl key -> boolSetting(key, false, Setting.Property.Deprecated, Setting.Property.Dynamic, Setting.Property.NodeScope), REMOTE_CLUSTERS_SEEDS); + public static final SettingUpgrader SEARCH_REMOTE_CLUSTER_SKIP_UNAVAILABLE_UPGRADER = new SettingUpgrader() { + + @Override + public Setting getSetting() { + return SEARCH_REMOTE_CLUSTER_SKIP_UNAVAILABLE; + } + + @Override + public String getKey(final String key) { + return key.replaceFirst("^search", "cluster"); + } + + }; + public static final Setting.AffixSetting REMOTE_CLUSTER_SKIP_UNAVAILABLE = Setting.affixKeySetting( "cluster.remote.", diff --git a/server/src/test/java/org/elasticsearch/common/settings/UpgradeSettingsIT.java b/server/src/test/java/org/elasticsearch/common/settings/UpgradeSettingsIT.java index 839b96e6418..99161f842b7 100644 --- a/server/src/test/java/org/elasticsearch/common/settings/UpgradeSettingsIT.java +++ b/server/src/test/java/org/elasticsearch/common/settings/UpgradeSettingsIT.java @@ -24,6 +24,7 @@ import org.elasticsearch.action.admin.cluster.state.ClusterStateResponse; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESSingleNodeTestCase; +import org.elasticsearch.transport.RemoteClusterService; import org.junit.After; import java.util.Arrays; @@ -122,4 +123,37 @@ public class UpgradeSettingsIT extends ESSingleNodeTestCase { assertThat(UpgradeSettingsPlugin.newSetting.get(settingsFunction.apply(response.getState().metaData())), equalTo("new." + value)); } + public void testUpgradeRemoteClusterSettings() { + final boolean skipUnavailable = randomBoolean(); + client() + .admin() + .cluster() + .prepareUpdateSettings() + .setPersistentSettings( + Settings.builder() + .put("search.remote.foo.skip_unavailable", skipUnavailable) + .putList("search.remote.foo.seeds", Collections.singletonList("localhost:9200")) + .put("search.remote.foo.proxy", "localhost:9200") + .build()) + .get(); + + final ClusterStateResponse response = client().admin().cluster().prepareState().clear().setMetaData(true).get(); + + final Settings settings = response.getState().metaData().persistentSettings(); + assertFalse(RemoteClusterService.SEARCH_REMOTE_CLUSTER_SKIP_UNAVAILABLE.getConcreteSettingForNamespace("foo").exists(settings)); + assertTrue(RemoteClusterService.REMOTE_CLUSTER_SKIP_UNAVAILABLE.getConcreteSettingForNamespace("foo").exists(settings)); + assertThat( + RemoteClusterService.REMOTE_CLUSTER_SKIP_UNAVAILABLE.getConcreteSettingForNamespace("foo").get(settings), + equalTo(skipUnavailable)); + assertFalse(RemoteClusterService.SEARCH_REMOTE_CLUSTERS_SEEDS.getConcreteSettingForNamespace("foo").exists(settings)); + assertTrue(RemoteClusterService.REMOTE_CLUSTERS_SEEDS.getConcreteSettingForNamespace("foo").exists(settings)); + assertThat( + RemoteClusterService.REMOTE_CLUSTERS_SEEDS.getConcreteSettingForNamespace("foo").get(settings), + equalTo(Collections.singletonList("localhost:9200"))); + assertFalse(RemoteClusterService.SEARCH_REMOTE_CLUSTERS_PROXY.getConcreteSettingForNamespace("foo").exists(settings)); + assertTrue(RemoteClusterService.REMOTE_CLUSTERS_PROXY.getConcreteSettingForNamespace("foo").exists(settings)); + assertThat( + RemoteClusterService.REMOTE_CLUSTERS_PROXY.getConcreteSettingForNamespace("foo").get(settings), equalTo("localhost:9200")); + } + } diff --git a/x-pack/qa/full-cluster-restart/build.gradle b/x-pack/qa/full-cluster-restart/build.gradle index 65c4573e9a0..c0fb7eb2b77 100644 --- a/x-pack/qa/full-cluster-restart/build.gradle +++ b/x-pack/qa/full-cluster-restart/build.gradle @@ -182,6 +182,7 @@ subprojects { systemProperty 'tests.old_cluster_version', version.toString().minus("-SNAPSHOT") systemProperty 'tests.path.repo', new File(buildDir, "cluster/shared/repo") exclude 'org/elasticsearch/upgrades/FullClusterRestartIT.class' + exclude 'org/elasticsearch/upgrades/FullClusterRestartSettingsUpgradeIT.class' exclude 'org/elasticsearch/upgrades/QueryBuilderBWCIT.class' } @@ -218,6 +219,7 @@ subprojects { systemProperty 'tests.old_cluster_version', version.toString().minus("-SNAPSHOT") systemProperty 'tests.path.repo', new File(buildDir, "cluster/shared/repo") exclude 'org/elasticsearch/upgrades/FullClusterRestartIT.class' + exclude 'org/elasticsearch/upgrades/FullClusterRestartSettingsUpgradeIT.class' exclude 'org/elasticsearch/upgrades/QueryBuilderBWCIT.class' } diff --git a/x-pack/qa/full-cluster-restart/src/test/java/org/elasticsearch/xpack/restart/FullClusterRestartSettingsUpgradeIT.java b/x-pack/qa/full-cluster-restart/src/test/java/org/elasticsearch/xpack/restart/FullClusterRestartSettingsUpgradeIT.java new file mode 100644 index 00000000000..a679604a546 --- /dev/null +++ b/x-pack/qa/full-cluster-restart/src/test/java/org/elasticsearch/xpack/restart/FullClusterRestartSettingsUpgradeIT.java @@ -0,0 +1,24 @@ +/* + * 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.restart; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +public class FullClusterRestartSettingsUpgradeIT extends org.elasticsearch.upgrades.FullClusterRestartSettingsUpgradeIT { + + @Override + protected Settings restClientSettings() { + final String token = + "Basic " + Base64.getEncoder().encodeToString("test_user:x-pack-test-password".getBytes(StandardCharsets.UTF_8)); + return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build(); + } + +} From dcdacd2f3fef8eb803cfd3256b9c7f83eb486e95 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Wed, 12 Sep 2018 01:19:05 -0400 Subject: [PATCH 21/78] Lower version on full cluster restart settings test The change to upgrade cross-cluster search settings was backported to 6.5.0. Therefore, this assumption needs to be reduced to the latest version where settings were not automatically upgraded. --- .../upgrades/FullClusterRestartSettingsUpgradeIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartSettingsUpgradeIT.java b/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartSettingsUpgradeIT.java index 8cb1ca717a8..19fbdc92fae 100644 --- a/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartSettingsUpgradeIT.java +++ b/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartSettingsUpgradeIT.java @@ -42,7 +42,7 @@ import static org.hamcrest.Matchers.equalTo; public class FullClusterRestartSettingsUpgradeIT extends AbstractFullClusterRestartTestCase { public void testRemoteClusterSettingsUpgraded() throws IOException { - assumeTrue("settings automatically upgraded since 7.0.0", getOldClusterVersion().before(Version.V_7_0_0_alpha1)); + assumeTrue("settings automatically upgraded since 6.5.0", getOldClusterVersion().before(Version.V_6_5_0)); if (isRunningAgainstOldCluster()) { final Request putSettingsRequest = new Request("PUT", "/_cluster/settings"); try (XContentBuilder builder = jsonBuilder()) { From 20476b9e062e57b73f4ddf14e889d4de6e2f9c88 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Wed, 12 Sep 2018 01:54:34 -0400 Subject: [PATCH 22/78] Disable CCR REST endpoints if CCR disabled (#33619) This commit avoids enabling the CCR REST endpoints if CCR is disabled. --- .../src/main/java/org/elasticsearch/xpack/ccr/Ccr.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java index 66b8a5c9590..39bed1ae771 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java @@ -163,6 +163,10 @@ public class Ccr extends Plugin implements ActionPlugin, PersistentTaskPlugin, E IndexScopedSettings indexScopedSettings, SettingsFilter settingsFilter, IndexNameExpressionResolver indexNameExpressionResolver, Supplier nodesInCluster) { + if (enabled == false) { + return emptyList(); + } + return Arrays.asList( // stats API new RestCcrStatsAction(settings, restController), @@ -228,10 +232,7 @@ public class Ccr extends Plugin implements ActionPlugin, PersistentTaskPlugin, E return Collections.emptyList(); } - FixedExecutorBuilder ccrTp = new FixedExecutorBuilder(settings, CCR_THREAD_POOL_NAME, - 32, 100, "xpack.ccr.ccr_thread_pool"); - - return Collections.singletonList(ccrTp); + return Collections.singletonList(new FixedExecutorBuilder(settings, CCR_THREAD_POOL_NAME, 32, 100, "xpack.ccr.ccr_thread_pool")); } protected XPackLicenseState getLicenseState() { return XPackPlugin.getSharedLicenseState(); } From 4561c5ee832d7a7dfe0ad3aeea3ff60ef0340fd8 Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Wed, 12 Sep 2018 08:47:32 +0200 Subject: [PATCH 23/78] Clarify context suggestions filtering and boosting (#33601) This change clarifies the documentation of the context completion suggester regarding filtering and boosting with contexts. Unlike the suggester v1, filtering on multiple contexts works as a disjunction, a suggestion matches if it contains at least one of the provided context values and boosting selects the maximum score among the matching contexts. This commit also adapts an old test that was written for the v1 suggester and commented out for version 2 because the behavior changed. --- .../suggesters/context-suggest.asciidoc | 23 +++++++++++++------ .../ContextCompletionSuggestSearchIT.java | 4 ++-- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/docs/reference/search/suggesters/context-suggest.asciidoc b/docs/reference/search/suggesters/context-suggest.asciidoc index 2b522062ec0..ab52097a4c5 100644 --- a/docs/reference/search/suggesters/context-suggest.asciidoc +++ b/docs/reference/search/suggesters/context-suggest.asciidoc @@ -13,6 +13,9 @@ Every context mapping has a unique name and a type. There are two types: `catego and `geo`. Context mappings are configured under the `contexts` parameter in the field mapping. +NOTE: It is mandatory to provide a context when indexing and querying + a context enabled completion field. + The following defines types, each with two context mappings for a completion field: @@ -84,10 +87,6 @@ PUT place_path_category NOTE: Adding context mappings increases the index size for completion field. The completion index is entirely heap resident, you can monitor the completion field index size using <>. -NOTE: deprecated[7.0.0, Indexing a suggestion without context on a context enabled completion field is deprecated -and will be removed in the next major release. If you want to index a suggestion that matches all contexts you should -add a special context for it.] - [[suggester-context-category]] [float] ==== Category Context @@ -160,9 +159,9 @@ POST place/_search?pretty // CONSOLE // TEST[continued] -Note: deprecated[7.0.0, When no categories are provided at query-time, all indexed documents are considered. -Querying with no categories on a category enabled completion field is deprecated and will be removed in the next major release -as it degrades search performance considerably.] +NOTE: If multiple categories or category contexts are set on the query +they are merged as a disjunction. This means that suggestions match +if they contain at least one of the provided context values. Suggestions with certain categories can be boosted higher than others. The following filters suggestions by categories and additionally boosts @@ -218,6 +217,9 @@ multiple category context clauses. The following parameters are supported for a so on, by specifying a category prefix of 'type'. Defaults to `false` +NOTE: If a suggestion entry matches multiple contexts the final score is computed as the +maximum score produced by any matching contexts. + [[suggester-context-geo]] [float] ==== Geo location Context @@ -307,6 +309,10 @@ POST place/_search NOTE: When a location with a lower precision at query time is specified, all suggestions that fall within the area will be considered. +NOTE: If multiple categories or category contexts are set on the query +they are merged as a disjunction. This means that suggestions match +if they contain at least one of the provided context values. + Suggestions that are within an area represented by a geohash can also be boosted higher than others, as shown by the following: @@ -349,6 +355,9 @@ POST place/_search?pretty that fall under the geohash representation of '(43.6624803, -79.3863353)' with a default precision of '6' by a factor of `2` +NOTE: If a suggestion entry matches multiple contexts the final score is computed as the +maximum score produced by any matching contexts. + In addition to accepting context values, a context query can be composed of multiple context clauses. The following parameters are supported for a `category` context clause: diff --git a/server/src/test/java/org/elasticsearch/search/suggest/ContextCompletionSuggestSearchIT.java b/server/src/test/java/org/elasticsearch/search/suggest/ContextCompletionSuggestSearchIT.java index 44c49ace5de..3c91cda5f86 100644 --- a/server/src/test/java/org/elasticsearch/search/suggest/ContextCompletionSuggestSearchIT.java +++ b/server/src/test/java/org/elasticsearch/search/suggest/ContextCompletionSuggestSearchIT.java @@ -275,7 +275,6 @@ public class ContextCompletionSuggestSearchIT extends ESIntegTestCase { assertSuggestions("foo", typeFilterSuggest, "suggestion9", "suggestion6", "suggestion5", "suggestion2", "suggestion1"); } - @AwaitsFix(bugUrl = "multiple context boosting is broken, as a suggestion, contexts pair is treated as (num(context) entries)") public void testMultiContextBoosting() throws Exception { LinkedHashMap> map = new LinkedHashMap<>(); map.put("cat", ContextBuilder.category("cat").field("cat").build()); @@ -328,7 +327,8 @@ public class ContextCompletionSuggestSearchIT extends ESIntegTestCase { CategoryQueryContext.builder().setCategory("cat1").build()) ); multiContextBoostSuggest.contexts(contextMap); - assertSuggestions("foo", multiContextBoostSuggest, "suggestion9", "suggestion6", "suggestion5", "suggestion2", "suggestion1"); + // the score of each suggestion is the maximum score among the matching contexts + assertSuggestions("foo", multiContextBoostSuggest, "suggestion9", "suggestion8", "suggestion5", "suggestion6", "suggestion4"); } public void testSeveralContexts() throws Exception { From c92ec1c5d7c516b83e6264528e88ce42e8f6913e Mon Sep 17 00:00:00 2001 From: lipsill <39668292+lipsill@users.noreply.github.com> Date: Wed, 12 Sep 2018 09:16:40 +0200 Subject: [PATCH 24/78] Forbid negative `weight` in Function Score Query (#33390) This change forbids negative `weight` in Function Score query. Negative scores are forbidden in Lucene 8. --- .../query/functionscore/ScoreFunctionBuilder.java | 12 +++++++++--- .../FunctionScoreQueryBuilderTests.java | 3 +++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionBuilder.java b/server/src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionBuilder.java index c64f1b1e403..6cfe7d177da 100644 --- a/server/src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionBuilder.java @@ -24,7 +24,6 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.lucene.search.function.ScoreFunction; import org.elasticsearch.common.lucene.search.function.WeightFactorFunction; -import org.elasticsearch.common.xcontent.ToXContent.Params; import org.elasticsearch.common.xcontent.ToXContentFragment; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.query.QueryShardContext; @@ -46,7 +45,7 @@ public abstract class ScoreFunctionBuilder> * Read from a stream. */ public ScoreFunctionBuilder(StreamInput in) throws IOException { - weight = in.readOptionalFloat(); + weight = checkWeight(in.readOptionalFloat()); } @Override @@ -70,10 +69,17 @@ public abstract class ScoreFunctionBuilder> */ @SuppressWarnings("unchecked") public final FB setWeight(float weight) { - this.weight = weight; + this.weight = checkWeight(weight); return (FB) this; } + private Float checkWeight(Float weight) { + if (weight != null && Float.compare(weight, 0) < 0) { + throw new IllegalArgumentException("[weight] cannot be negative for a filtering function"); + } + return weight; + } + /** * The weight applied to the function before combining. */ diff --git a/server/src/test/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilderTests.java index cc224019100..624205a1a3c 100644 --- a/server/src/test/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilderTests.java @@ -20,6 +20,7 @@ package org.elasticsearch.index.query.functionscore; import com.fasterxml.jackson.core.JsonParseException; + import org.apache.lucene.index.Term; import org.apache.lucene.search.MatchAllDocsQuery; import org.apache.lucene.search.Query; @@ -272,6 +273,8 @@ public class FunctionScoreQueryBuilderTests extends AbstractQueryTestCase builder.scoreMode(null)); expectThrows(IllegalArgumentException.class, () -> builder.boostMode(null)); + expectThrows(IllegalArgumentException.class, + () -> new FunctionScoreQueryBuilder.FilterFunctionBuilder(new WeightBuilder().setWeight(-1 * randomFloat()))); } public void testParseFunctionsArray() throws IOException { From 96c49e5ed066bcaa82392c1acdced94ca43b28bd Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Wed, 12 Sep 2018 12:49:51 +0200 Subject: [PATCH 25/78] [CCR] Improve shard follow task's retryable error handling (#33371) Improve failure handling of retryable errors by retrying remote calls in a exponential backoff like manner. The delay between a retry would not be longer than the configured max retry delay. Also retryable errors will be retried indefinitely. Relates to #30086 --- .../action/PutAutoFollowPatternAction.java | 26 ++-- .../xpack/ccr/action/ShardFollowNodeTask.java | 33 +++-- .../xpack/ccr/action/ShardFollowTask.java | 26 ++-- .../action/TransportFollowIndexAction.java | 2 +- .../TransportPutAutoFollowPatternAction.java | 2 +- .../xpack/ccr/action/AutoFollowTests.java | 6 +- .../PutAutoFollowPatternRequestTests.java | 2 +- .../ccr/action/ShardFollowNodeTaskTests.java | 138 ++++-------------- .../core/ccr/action/FollowIndexAction.java | 29 ++-- 9 files changed, 92 insertions(+), 172 deletions(-) diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/PutAutoFollowPatternAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/PutAutoFollowPatternAction.java index a01fd8e3bc2..eb23244722d 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/PutAutoFollowPatternAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/PutAutoFollowPatternAction.java @@ -56,9 +56,9 @@ public class PutAutoFollowPatternAction extends Action { PARSER.declareLong(Request::setMaxOperationSizeInBytes, AutoFollowPattern.MAX_BATCH_SIZE_IN_BYTES); PARSER.declareInt(Request::setMaxConcurrentWriteBatches, AutoFollowPattern.MAX_CONCURRENT_WRITE_BATCHES); PARSER.declareInt(Request::setMaxWriteBufferSize, AutoFollowPattern.MAX_WRITE_BUFFER_SIZE); - PARSER.declareField(Request::setRetryTimeout, + PARSER.declareField(Request::setMaxRetryDelay, (p, c) -> TimeValue.parseTimeValue(p.text(), AutoFollowPattern.RETRY_TIMEOUT.getPreferredName()), - ShardFollowTask.RETRY_TIMEOUT, ObjectParser.ValueType.STRING); + ShardFollowTask.MAX_RETRY_DELAY, ObjectParser.ValueType.STRING); PARSER.declareField(Request::setIdleShardRetryDelay, (p, c) -> TimeValue.parseTimeValue(p.text(), AutoFollowPattern.IDLE_SHARD_RETRY_DELAY.getPreferredName()), ShardFollowTask.IDLE_SHARD_RETRY_DELAY, ObjectParser.ValueType.STRING); @@ -87,7 +87,7 @@ public class PutAutoFollowPatternAction extends Action { private Long maxOperationSizeInBytes; private Integer maxConcurrentWriteBatches; private Integer maxWriteBufferSize; - private TimeValue retryTimeout; + private TimeValue maxRetryDelay; private TimeValue idleShardRetryDelay; @Override @@ -166,12 +166,12 @@ public class PutAutoFollowPatternAction extends Action { this.maxWriteBufferSize = maxWriteBufferSize; } - public TimeValue getRetryTimeout() { - return retryTimeout; + public TimeValue getMaxRetryDelay() { + return maxRetryDelay; } - public void setRetryTimeout(TimeValue retryTimeout) { - this.retryTimeout = retryTimeout; + public void setMaxRetryDelay(TimeValue maxRetryDelay) { + this.maxRetryDelay = maxRetryDelay; } public TimeValue getIdleShardRetryDelay() { @@ -193,7 +193,7 @@ public class PutAutoFollowPatternAction extends Action { maxOperationSizeInBytes = in.readOptionalLong(); maxConcurrentWriteBatches = in.readOptionalVInt(); maxWriteBufferSize = in.readOptionalVInt(); - retryTimeout = in.readOptionalTimeValue(); + maxRetryDelay = in.readOptionalTimeValue(); idleShardRetryDelay = in.readOptionalTimeValue(); } @@ -208,7 +208,7 @@ public class PutAutoFollowPatternAction extends Action { out.writeOptionalLong(maxOperationSizeInBytes); out.writeOptionalVInt(maxConcurrentWriteBatches); out.writeOptionalVInt(maxWriteBufferSize); - out.writeOptionalTimeValue(retryTimeout); + out.writeOptionalTimeValue(maxRetryDelay); out.writeOptionalTimeValue(idleShardRetryDelay); } @@ -236,8 +236,8 @@ public class PutAutoFollowPatternAction extends Action { if (maxConcurrentWriteBatches != null) { builder.field(ShardFollowTask.MAX_CONCURRENT_WRITE_BATCHES.getPreferredName(), maxConcurrentWriteBatches); } - if (retryTimeout != null) { - builder.field(ShardFollowTask.RETRY_TIMEOUT.getPreferredName(), retryTimeout.getStringRep()); + if (maxRetryDelay != null) { + builder.field(ShardFollowTask.MAX_RETRY_DELAY.getPreferredName(), maxRetryDelay.getStringRep()); } if (idleShardRetryDelay != null) { builder.field(ShardFollowTask.IDLE_SHARD_RETRY_DELAY.getPreferredName(), idleShardRetryDelay.getStringRep()); @@ -260,7 +260,7 @@ public class PutAutoFollowPatternAction extends Action { Objects.equals(maxOperationSizeInBytes, request.maxOperationSizeInBytes) && Objects.equals(maxConcurrentWriteBatches, request.maxConcurrentWriteBatches) && Objects.equals(maxWriteBufferSize, request.maxWriteBufferSize) && - Objects.equals(retryTimeout, request.retryTimeout) && + Objects.equals(maxRetryDelay, request.maxRetryDelay) && Objects.equals(idleShardRetryDelay, request.idleShardRetryDelay); } @@ -275,7 +275,7 @@ public class PutAutoFollowPatternAction extends Action { maxOperationSizeInBytes, maxConcurrentWriteBatches, maxWriteBufferSize, - retryTimeout, + maxRetryDelay, idleShardRetryDelay ); } diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTask.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTask.java index 0a0a6877dc9..c221c097977 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTask.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTask.java @@ -10,6 +10,7 @@ import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.support.TransportActions; +import org.elasticsearch.common.Randomness; import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.transport.NetworkExceptionHelper; import org.elasticsearch.common.unit.TimeValue; @@ -18,7 +19,6 @@ import org.elasticsearch.index.translog.Translog; import org.elasticsearch.persistent.AllocatedPersistentTask; import org.elasticsearch.tasks.TaskId; import org.elasticsearch.xpack.ccr.action.bulk.BulkShardOperationsResponse; -import org.elasticsearch.xpack.core.ccr.action.FollowIndexAction; import org.elasticsearch.xpack.core.ccr.ShardFollowNodeTaskStatus; import java.util.ArrayList; @@ -43,11 +43,12 @@ import java.util.function.LongSupplier; */ public abstract class ShardFollowNodeTask extends AllocatedPersistentTask { + private static final int DELAY_MILLIS = 50; private static final Logger LOGGER = Loggers.getLogger(ShardFollowNodeTask.class); private final String leaderIndex; private final ShardFollowTask params; - private final TimeValue retryTimeout; + private final TimeValue maxRetryDelay; private final TimeValue idleShardChangesRequestDelay; private final BiConsumer scheduler; private final LongSupplier relativeTimeProvider; @@ -79,7 +80,7 @@ public abstract class ShardFollowNodeTask extends AllocatedPersistentTask { this.params = params; this.scheduler = scheduler; this.relativeTimeProvider = relativeTimeProvider; - this.retryTimeout = params.getRetryTimeout(); + this.maxRetryDelay = params.getMaxRetryDelay(); this.idleShardChangesRequestDelay = params.getIdleShardRetryDelay(); /* * We keep track of the most recent fetch exceptions, with the number of exceptions that we track equal to the maximum number of @@ -357,20 +358,28 @@ public abstract class ShardFollowNodeTask extends AllocatedPersistentTask { private void handleFailure(Exception e, AtomicInteger retryCounter, Runnable task) { assert e != null; - if (shouldRetry(e)) { - if (isStopped() == false && retryCounter.incrementAndGet() <= FollowIndexAction.RETRY_LIMIT) { - LOGGER.debug(new ParameterizedMessage("{} error during follow shard task, retrying...", params.getFollowShardId()), e); - scheduler.accept(retryTimeout, task); - } else { - markAsFailed(new ElasticsearchException("retrying failed [" + retryCounter.get() + - "] times, aborting...", e)); - } + if (shouldRetry(e) && isStopped() == false) { + int currentRetry = retryCounter.incrementAndGet(); + LOGGER.debug(new ParameterizedMessage("{} error during follow shard task, retrying [{}]", + params.getFollowShardId(), currentRetry), e); + long delay = computeDelay(currentRetry, maxRetryDelay.getMillis()); + scheduler.accept(TimeValue.timeValueMillis(delay), task); } else { markAsFailed(e); } } - private boolean shouldRetry(Exception e) { + static long computeDelay(int currentRetry, long maxRetryDelayInMillis) { + // Cap currentRetry to avoid overflow when computing n variable + int maxCurrentRetry = Math.min(currentRetry, 24); + long n = Math.round(Math.pow(2, maxCurrentRetry - 1)); + // + 1 here, because nextInt(...) bound is exclusive and otherwise the first delay would always be zero. + int k = Randomness.get().nextInt(Math.toIntExact(n + 1)); + int backOffDelay = k * DELAY_MILLIS; + return Math.min(backOffDelay, maxRetryDelayInMillis); + } + + private static boolean shouldRetry(Exception e) { return NetworkExceptionHelper.isConnectException(e) || NetworkExceptionHelper.isCloseConnectionException(e) || TransportActions.isShardNotAvailableException(e); diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowTask.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowTask.java index 82482792f39..9da19cb1998 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowTask.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowTask.java @@ -48,7 +48,7 @@ public class ShardFollowTask implements XPackPlugin.XPackPersistentTaskParams { public static final ParseField MAX_BATCH_SIZE_IN_BYTES = new ParseField("max_batch_size_in_bytes"); public static final ParseField MAX_CONCURRENT_WRITE_BATCHES = new ParseField("max_concurrent_write_batches"); public static final ParseField MAX_WRITE_BUFFER_SIZE = new ParseField("max_write_buffer_size"); - public static final ParseField RETRY_TIMEOUT = new ParseField("retry_timeout"); + public static final ParseField MAX_RETRY_DELAY = new ParseField("max_retry_delay"); public static final ParseField IDLE_SHARD_RETRY_DELAY = new ParseField("idle_shard_retry_delay"); @SuppressWarnings("unchecked") @@ -71,8 +71,8 @@ public class ShardFollowTask implements XPackPlugin.XPackPersistentTaskParams { PARSER.declareInt(ConstructingObjectParser.constructorArg(), MAX_CONCURRENT_WRITE_BATCHES); PARSER.declareInt(ConstructingObjectParser.constructorArg(), MAX_WRITE_BUFFER_SIZE); PARSER.declareField(ConstructingObjectParser.constructorArg(), - (p, c) -> TimeValue.parseTimeValue(p.text(), RETRY_TIMEOUT.getPreferredName()), - RETRY_TIMEOUT, ObjectParser.ValueType.STRING); + (p, c) -> TimeValue.parseTimeValue(p.text(), MAX_RETRY_DELAY.getPreferredName()), + MAX_RETRY_DELAY, ObjectParser.ValueType.STRING); PARSER.declareField(ConstructingObjectParser.constructorArg(), (p, c) -> TimeValue.parseTimeValue(p.text(), IDLE_SHARD_RETRY_DELAY.getPreferredName()), IDLE_SHARD_RETRY_DELAY, ObjectParser.ValueType.STRING); @@ -87,13 +87,13 @@ public class ShardFollowTask implements XPackPlugin.XPackPersistentTaskParams { private final long maxBatchSizeInBytes; private final int maxConcurrentWriteBatches; private final int maxWriteBufferSize; - private final TimeValue retryTimeout; + private final TimeValue maxRetryDelay; private final TimeValue idleShardRetryDelay; private final Map headers; ShardFollowTask(String leaderClusterAlias, ShardId followShardId, ShardId leaderShardId, int maxBatchOperationCount, int maxConcurrentReadBatches, long maxBatchSizeInBytes, int maxConcurrentWriteBatches, - int maxWriteBufferSize, TimeValue retryTimeout, TimeValue idleShardRetryDelay, Map headers) { + int maxWriteBufferSize, TimeValue maxRetryDelay, TimeValue idleShardRetryDelay, Map headers) { this.leaderClusterAlias = leaderClusterAlias; this.followShardId = followShardId; this.leaderShardId = leaderShardId; @@ -102,7 +102,7 @@ public class ShardFollowTask implements XPackPlugin.XPackPersistentTaskParams { this.maxBatchSizeInBytes = maxBatchSizeInBytes; this.maxConcurrentWriteBatches = maxConcurrentWriteBatches; this.maxWriteBufferSize = maxWriteBufferSize; - this.retryTimeout = retryTimeout; + this.maxRetryDelay = maxRetryDelay; this.idleShardRetryDelay = idleShardRetryDelay; this.headers = headers != null ? Collections.unmodifiableMap(headers) : Collections.emptyMap(); } @@ -116,7 +116,7 @@ public class ShardFollowTask implements XPackPlugin.XPackPersistentTaskParams { this.maxBatchSizeInBytes = in.readVLong(); this.maxConcurrentWriteBatches = in.readVInt(); this.maxWriteBufferSize = in.readVInt(); - this.retryTimeout = in.readTimeValue(); + this.maxRetryDelay = in.readTimeValue(); this.idleShardRetryDelay = in.readTimeValue(); this.headers = Collections.unmodifiableMap(in.readMap(StreamInput::readString, StreamInput::readString)); } @@ -153,8 +153,8 @@ public class ShardFollowTask implements XPackPlugin.XPackPersistentTaskParams { return maxBatchSizeInBytes; } - public TimeValue getRetryTimeout() { - return retryTimeout; + public TimeValue getMaxRetryDelay() { + return maxRetryDelay; } public TimeValue getIdleShardRetryDelay() { @@ -184,7 +184,7 @@ public class ShardFollowTask implements XPackPlugin.XPackPersistentTaskParams { out.writeVLong(maxBatchSizeInBytes); out.writeVInt(maxConcurrentWriteBatches); out.writeVInt(maxWriteBufferSize); - out.writeTimeValue(retryTimeout); + out.writeTimeValue(maxRetryDelay); out.writeTimeValue(idleShardRetryDelay); out.writeMap(headers, StreamOutput::writeString, StreamOutput::writeString); } @@ -210,7 +210,7 @@ public class ShardFollowTask implements XPackPlugin.XPackPersistentTaskParams { builder.field(MAX_BATCH_SIZE_IN_BYTES.getPreferredName(), maxBatchSizeInBytes); builder.field(MAX_CONCURRENT_WRITE_BATCHES.getPreferredName(), maxConcurrentWriteBatches); builder.field(MAX_WRITE_BUFFER_SIZE.getPreferredName(), maxWriteBufferSize); - builder.field(RETRY_TIMEOUT.getPreferredName(), retryTimeout.getStringRep()); + builder.field(MAX_RETRY_DELAY.getPreferredName(), maxRetryDelay.getStringRep()); builder.field(IDLE_SHARD_RETRY_DELAY.getPreferredName(), idleShardRetryDelay.getStringRep()); builder.field(HEADERS.getPreferredName(), headers); return builder.endObject(); @@ -229,7 +229,7 @@ public class ShardFollowTask implements XPackPlugin.XPackPersistentTaskParams { maxConcurrentWriteBatches == that.maxConcurrentWriteBatches && maxBatchSizeInBytes == that.maxBatchSizeInBytes && maxWriteBufferSize == that.maxWriteBufferSize && - Objects.equals(retryTimeout, that.retryTimeout) && + Objects.equals(maxRetryDelay, that.maxRetryDelay) && Objects.equals(idleShardRetryDelay, that.idleShardRetryDelay) && Objects.equals(headers, that.headers); } @@ -237,7 +237,7 @@ public class ShardFollowTask implements XPackPlugin.XPackPersistentTaskParams { @Override public int hashCode() { return Objects.hash(leaderClusterAlias, followShardId, leaderShardId, maxBatchOperationCount, maxConcurrentReadBatches, - maxConcurrentWriteBatches, maxBatchSizeInBytes, maxWriteBufferSize, retryTimeout, idleShardRetryDelay, headers); + maxConcurrentWriteBatches, maxBatchSizeInBytes, maxWriteBufferSize, maxRetryDelay, idleShardRetryDelay, headers); } public String toString() { diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexAction.java index 33447ef4208..3128a63f24b 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexAction.java @@ -175,7 +175,7 @@ public class TransportFollowIndexAction extends HandledTransportAction { - assertThat(status.numberOfFailedFetches(), equalTo(retryCounter.get())); - if (retryCounter.get() > 0) { - assertThat(status.fetchExceptions().entrySet(), hasSize(1)); - final Map.Entry entry = status.fetchExceptions().entrySet().iterator().next(); - assertThat(entry.getKey(), equalTo(0L)); - assertThat(entry.getValue(), instanceOf(ElasticsearchException.class)); - assertNotNull(entry.getValue().getCause()); - assertThat(entry.getValue().getCause(), instanceOf(ShardNotFoundException.class)); - final ShardNotFoundException cause = (ShardNotFoundException) entry.getValue().getCause(); - assertThat(cause.getShardId().getIndexName(), equalTo("leader_index")); - assertThat(cause.getShardId().getId(), equalTo(0)); - } - retryCounter.incrementAndGet(); - }; - task.coordinateReads(); - - assertThat(shardChangesRequests.size(), equalTo(11)); - for (long[] shardChangesRequest : shardChangesRequests) { - assertThat(shardChangesRequest[0], equalTo(0L)); - assertThat(shardChangesRequest[1], equalTo(64L)); - } - - assertTrue("task is stopped", task.isStopped()); - assertThat(fatalError, notNullValue()); - assertThat(fatalError.getMessage(), containsString("retrying failed [")); - ShardFollowNodeTaskStatus status = task.getStatus(); - assertThat(status.numberOfConcurrentReads(), equalTo(1)); - assertThat(status.numberOfConcurrentWrites(), equalTo(0)); - assertThat(status.numberOfFailedFetches(), equalTo(11L)); - assertThat(status.fetchExceptions().entrySet(), hasSize(1)); - final Map.Entry entry = status.fetchExceptions().entrySet().iterator().next(); - assertThat(entry.getKey(), equalTo(0L)); - assertThat(entry.getValue(), instanceOf(ElasticsearchException.class)); - assertNotNull(entry.getValue().getCause()); - assertThat(entry.getValue().getCause(), instanceOf(ShardNotFoundException.class)); - final ShardNotFoundException cause = (ShardNotFoundException) entry.getValue().getCause(); - assertThat(cause.getShardId().getIndexName(), equalTo("leader_index")); - assertThat(cause.getShardId().getId(), equalTo(0)); - assertThat(status.lastRequestedSeqNo(), equalTo(63L)); - assertThat(status.leaderGlobalCheckpoint(), equalTo(63L)); - } - public void testReceiveNonRetryableError() { ShardFollowNodeTask task = createShardFollowTask(64, 1, 1, Integer.MAX_VALUE, Long.MAX_VALUE); startTask(task, 63, -1); @@ -455,7 +403,7 @@ public class ShardFollowNodeTaskTests extends ESTestCase { ShardFollowNodeTask task = createShardFollowTask(64, 1, 1, Integer.MAX_VALUE, Long.MAX_VALUE); startTask(task, 63, -1); - int max = randomIntBetween(1, 10); + int max = randomIntBetween(1, 30); for (int i = 0; i < max; i++) { mappingUpdateFailures.add(new ConnectException()); } @@ -476,31 +424,6 @@ public class ShardFollowNodeTaskTests extends ESTestCase { } - public void testMappingUpdateRetryableErrorRetriedTooManyTimes() { - ShardFollowNodeTask task = createShardFollowTask(64, 1, 1, Integer.MAX_VALUE, Long.MAX_VALUE); - startTask(task, 63, -1); - - int max = randomIntBetween(11, 20); - for (int i = 0; i < max; i++) { - mappingUpdateFailures.add(new ConnectException()); - } - mappingVersions.add(1L); - task.coordinateReads(); - ShardChangesAction.Response response = generateShardChangesResponse(0, 64, 1L, 64L); - task.handleReadResponse(0L, 64L, response); - - assertThat(mappingUpdateFailures.size(), equalTo(max - 11)); - assertThat(mappingVersions.size(), equalTo(1)); - assertThat(bulkShardOperationRequests.size(), equalTo(0)); - assertThat(task.isStopped(), equalTo(true)); - ShardFollowNodeTaskStatus status = task.getStatus(); - assertThat(status.mappingVersion(), equalTo(0L)); - assertThat(status.numberOfConcurrentReads(), equalTo(1)); - assertThat(status.numberOfConcurrentWrites(), equalTo(0)); - assertThat(status.lastRequestedSeqNo(), equalTo(63L)); - assertThat(status.leaderGlobalCheckpoint(), equalTo(63L)); - } - public void testMappingUpdateNonRetryableError() { ShardFollowNodeTask task = createShardFollowTask(64, 1, 1, Integer.MAX_VALUE, Long.MAX_VALUE); startTask(task, 63, -1); @@ -597,7 +520,7 @@ public class ShardFollowNodeTaskTests extends ESTestCase { assertThat(shardChangesRequests.get(0)[0], equalTo(0L)); assertThat(shardChangesRequests.get(0)[1], equalTo(64L)); - int max = randomIntBetween(1, 10); + int max = randomIntBetween(1, 30); for (int i = 0; i < max; i++) { writeFailures.add(new ShardNotFoundException(new ShardId("leader_index", "", 0))); } @@ -616,34 +539,6 @@ public class ShardFollowNodeTaskTests extends ESTestCase { assertThat(status.followerGlobalCheckpoint(), equalTo(-1L)); } - public void testRetryableErrorRetriedTooManyTimes() { - ShardFollowNodeTask task = createShardFollowTask(64, 1, 1, Integer.MAX_VALUE, Long.MAX_VALUE); - startTask(task, 63, -1); - - task.coordinateReads(); - assertThat(shardChangesRequests.size(), equalTo(1)); - assertThat(shardChangesRequests.get(0)[0], equalTo(0L)); - assertThat(shardChangesRequests.get(0)[1], equalTo(64L)); - - int max = randomIntBetween(11, 32); - for (int i = 0; i < max; i++) { - writeFailures.add(new ShardNotFoundException(new ShardId("leader_index", "", 0))); - } - ShardChangesAction.Response response = generateShardChangesResponse(0, 63, 0L, 643); - // Also invokes coordinatesWrites() - task.innerHandleReadResponse(0L, 63L, response); - - // Number of requests is equal to initial request + retried attempts: - assertThat(bulkShardOperationRequests.size(), equalTo(11)); - for (List operations : bulkShardOperationRequests) { - assertThat(operations, equalTo(Arrays.asList(response.getOperations()))); - } - assertThat(task.isStopped(), equalTo(true)); - ShardFollowNodeTaskStatus status = task.getStatus(); - assertThat(status.numberOfConcurrentWrites(), equalTo(1)); - assertThat(status.followerGlobalCheckpoint(), equalTo(-1L)); - } - public void testNonRetryableError() { ShardFollowNodeTask task = createShardFollowTask(64, 1, 1, Integer.MAX_VALUE, Long.MAX_VALUE); startTask(task, 63, -1); @@ -712,8 +607,25 @@ public class ShardFollowNodeTaskTests extends ESTestCase { assertThat(status.followerGlobalCheckpoint(), equalTo(63L)); } - ShardFollowNodeTask createShardFollowTask(int maxBatchOperationCount, int maxConcurrentReadBatches, int maxConcurrentWriteBatches, - int bufferWriteLimit, long maxBatchSizeInBytes) { + public void testComputeDelay() { + long maxDelayInMillis = 1000; + assertThat(ShardFollowNodeTask.computeDelay(0, maxDelayInMillis), allOf(greaterThanOrEqualTo(0L), lessThanOrEqualTo(50L))); + assertThat(ShardFollowNodeTask.computeDelay(1, maxDelayInMillis), allOf(greaterThanOrEqualTo(0L), lessThanOrEqualTo(50L))); + assertThat(ShardFollowNodeTask.computeDelay(2, maxDelayInMillis), allOf(greaterThanOrEqualTo(0L), lessThanOrEqualTo(100L))); + assertThat(ShardFollowNodeTask.computeDelay(3, maxDelayInMillis), allOf(greaterThanOrEqualTo(0L), lessThanOrEqualTo(200L))); + assertThat(ShardFollowNodeTask.computeDelay(4, maxDelayInMillis), allOf(greaterThanOrEqualTo(0L), lessThanOrEqualTo(400L))); + assertThat(ShardFollowNodeTask.computeDelay(5, maxDelayInMillis), allOf(greaterThanOrEqualTo(0L), lessThanOrEqualTo(800L))); + assertThat(ShardFollowNodeTask.computeDelay(6, maxDelayInMillis), allOf(greaterThanOrEqualTo(0L), lessThanOrEqualTo(1000L))); + assertThat(ShardFollowNodeTask.computeDelay(7, maxDelayInMillis), allOf(greaterThanOrEqualTo(0L), lessThanOrEqualTo(1000L))); + assertThat(ShardFollowNodeTask.computeDelay(8, maxDelayInMillis), allOf(greaterThanOrEqualTo(0L), lessThanOrEqualTo(1000L))); + assertThat(ShardFollowNodeTask.computeDelay(1024, maxDelayInMillis), allOf(greaterThanOrEqualTo(0L), lessThanOrEqualTo(1000L))); + } + + private ShardFollowNodeTask createShardFollowTask(int maxBatchOperationCount, + int maxConcurrentReadBatches, + int maxConcurrentWriteBatches, + int bufferWriteLimit, + long maxBatchSizeInBytes) { AtomicBoolean stopped = new AtomicBoolean(false); ShardFollowTask params = new ShardFollowTask(null, new ShardId("follow_index", "", 0), new ShardId("leader_index", "", 0), maxBatchOperationCount, maxConcurrentReadBatches, maxBatchSizeInBytes, diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/FollowIndexAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/FollowIndexAction.java index c42ef8db9c1..2c311356d49 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/FollowIndexAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/FollowIndexAction.java @@ -33,7 +33,6 @@ public final class FollowIndexAction extends Action { public static final int DEFAULT_MAX_CONCURRENT_READ_BATCHES = 1; public static final int DEFAULT_MAX_CONCURRENT_WRITE_BATCHES = 1; public static final long DEFAULT_MAX_BATCH_SIZE_IN_BYTES = Long.MAX_VALUE; - public static final int RETRY_LIMIT = 10; public static final TimeValue DEFAULT_RETRY_TIMEOUT = new TimeValue(500); public static final TimeValue DEFAULT_IDLE_SHARD_RETRY_DELAY = TimeValue.timeValueSeconds(10); @@ -55,7 +54,7 @@ public final class FollowIndexAction extends Action { private static final ParseField MAX_BATCH_SIZE_IN_BYTES = new ParseField("max_batch_size_in_bytes"); private static final ParseField MAX_CONCURRENT_WRITE_BATCHES = new ParseField("max_concurrent_write_batches"); private static final ParseField MAX_WRITE_BUFFER_SIZE = new ParseField("max_write_buffer_size"); - private static final ParseField RETRY_TIMEOUT = new ParseField("retry_timeout"); + private static final ParseField MAX_RETRY_DELAY = new ParseField("max_retry_delay"); private static final ParseField IDLE_SHARD_RETRY_DELAY = new ParseField("idle_shard_retry_delay"); private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, true, (args, followerIndex) -> { @@ -76,8 +75,8 @@ public final class FollowIndexAction extends Action { PARSER.declareInt(ConstructingObjectParser.optionalConstructorArg(), MAX_WRITE_BUFFER_SIZE); PARSER.declareField( ConstructingObjectParser.optionalConstructorArg(), - (p, c) -> TimeValue.parseTimeValue(p.text(), RETRY_TIMEOUT.getPreferredName()), - RETRY_TIMEOUT, + (p, c) -> TimeValue.parseTimeValue(p.text(), MAX_RETRY_DELAY.getPreferredName()), + MAX_RETRY_DELAY, ObjectParser.ValueType.STRING); PARSER.declareField( ConstructingObjectParser.optionalConstructorArg(), @@ -143,10 +142,10 @@ public final class FollowIndexAction extends Action { return maxWriteBufferSize; } - private TimeValue retryTimeout; + private TimeValue maxRetryDelay; - public TimeValue getRetryTimeout() { - return retryTimeout; + public TimeValue getMaxRetryDelay() { + return maxRetryDelay; } private TimeValue idleShardRetryDelay; @@ -163,7 +162,7 @@ public final class FollowIndexAction extends Action { final Long maxOperationSizeInBytes, final Integer maxConcurrentWriteBatches, final Integer maxWriteBufferSize, - final TimeValue retryTimeout, + final TimeValue maxRetryDelay, final TimeValue idleShardRetryDelay) { if (leaderIndex == null) { @@ -203,7 +202,7 @@ public final class FollowIndexAction extends Action { throw new IllegalArgumentException(MAX_WRITE_BUFFER_SIZE.getPreferredName() + " must be larger than 0"); } - final TimeValue actualRetryTimeout = retryTimeout == null ? DEFAULT_RETRY_TIMEOUT : retryTimeout; + final TimeValue actualRetryTimeout = maxRetryDelay == null ? DEFAULT_RETRY_TIMEOUT : maxRetryDelay; final TimeValue actualIdleShardRetryDelay = idleShardRetryDelay == null ? DEFAULT_IDLE_SHARD_RETRY_DELAY : idleShardRetryDelay; this.leaderIndex = leaderIndex; @@ -213,7 +212,7 @@ public final class FollowIndexAction extends Action { this.maxOperationSizeInBytes = actualMaxOperationSizeInBytes; this.maxConcurrentWriteBatches = actualMaxConcurrentWriteBatches; this.maxWriteBufferSize = actualMaxWriteBufferSize; - this.retryTimeout = actualRetryTimeout; + this.maxRetryDelay = actualRetryTimeout; this.idleShardRetryDelay = actualIdleShardRetryDelay; } @@ -236,7 +235,7 @@ public final class FollowIndexAction extends Action { maxOperationSizeInBytes = in.readVLong(); maxConcurrentWriteBatches = in.readVInt(); maxWriteBufferSize = in.readVInt(); - retryTimeout = in.readOptionalTimeValue(); + maxRetryDelay = in.readOptionalTimeValue(); idleShardRetryDelay = in.readOptionalTimeValue(); } @@ -250,7 +249,7 @@ public final class FollowIndexAction extends Action { out.writeVLong(maxOperationSizeInBytes); out.writeVInt(maxConcurrentWriteBatches); out.writeVInt(maxWriteBufferSize); - out.writeOptionalTimeValue(retryTimeout); + out.writeOptionalTimeValue(maxRetryDelay); out.writeOptionalTimeValue(idleShardRetryDelay); } @@ -265,7 +264,7 @@ public final class FollowIndexAction extends Action { builder.field(MAX_WRITE_BUFFER_SIZE.getPreferredName(), maxWriteBufferSize); builder.field(MAX_CONCURRENT_READ_BATCHES.getPreferredName(), maxConcurrentReadBatches); builder.field(MAX_CONCURRENT_WRITE_BATCHES.getPreferredName(), maxConcurrentWriteBatches); - builder.field(RETRY_TIMEOUT.getPreferredName(), retryTimeout.getStringRep()); + builder.field(MAX_RETRY_DELAY.getPreferredName(), maxRetryDelay.getStringRep()); builder.field(IDLE_SHARD_RETRY_DELAY.getPreferredName(), idleShardRetryDelay.getStringRep()); } builder.endObject(); @@ -282,7 +281,7 @@ public final class FollowIndexAction extends Action { maxOperationSizeInBytes == request.maxOperationSizeInBytes && maxConcurrentWriteBatches == request.maxConcurrentWriteBatches && maxWriteBufferSize == request.maxWriteBufferSize && - Objects.equals(retryTimeout, request.retryTimeout) && + Objects.equals(maxRetryDelay, request.maxRetryDelay) && Objects.equals(idleShardRetryDelay, request.idleShardRetryDelay) && Objects.equals(leaderIndex, request.leaderIndex) && Objects.equals(followerIndex, request.followerIndex); @@ -298,7 +297,7 @@ public final class FollowIndexAction extends Action { maxOperationSizeInBytes, maxConcurrentWriteBatches, maxWriteBufferSize, - retryTimeout, + maxRetryDelay, idleShardRetryDelay ); } From d9bbb89b2611c8edcba7c81bde67578241af6136 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Wed, 12 Sep 2018 08:24:11 -0400 Subject: [PATCH 26/78] TEST: Adjust rollback condition when shard is empty If a shard is empty, it won't rollback its engine on promotion. This commit adjusts the expectation in the rollback test. Relates #33473 --- .../java/org/elasticsearch/index/shard/IndexShardTests.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java index d6b80a6f6d7..0c5d9b1613f 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java @@ -963,7 +963,8 @@ public class IndexShardTests extends IndexShardTestCase { Set docsBelowGlobalCheckpoint = getShardDocUIDs(indexShard).stream() .filter(id -> Long.parseLong(id) <= Math.max(globalCheckpointOnReplica, globalCheckpoint)).collect(Collectors.toSet()); final CountDownLatch latch = new CountDownLatch(1); - final boolean shouldRollback = Math.max(globalCheckpoint, globalCheckpointOnReplica) < indexShard.seqNoStats().getMaxSeqNo(); + final boolean shouldRollback = Math.max(globalCheckpoint, globalCheckpointOnReplica) < indexShard.seqNoStats().getMaxSeqNo() + && indexShard.seqNoStats().getMaxSeqNo() != SequenceNumbers.NO_OPS_PERFORMED; final Engine beforeRollbackEngine = indexShard.getEngine(); indexShard.acquireReplicaOperationPermit( indexShard.pendingPrimaryTerm + 1, From 0b567c0eeba3ad4c89e349f7f5439292a1f4356f Mon Sep 17 00:00:00 2001 From: Joel Green Date: Wed, 12 Sep 2018 13:34:05 +0100 Subject: [PATCH 27/78] =?UTF-8?q?[Docs]=C2=A0Update=20match-query.asciidoc?= =?UTF-8?q?=20(#33610)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/reference/query-dsl/match-query.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/query-dsl/match-query.asciidoc b/docs/reference/query-dsl/match-query.asciidoc index acff4d3b036..5c397d603be 100644 --- a/docs/reference/query-dsl/match-query.asciidoc +++ b/docs/reference/query-dsl/match-query.asciidoc @@ -172,7 +172,7 @@ GET /_search The example above creates a boolean query: -`(ny OR (new AND york)) city)` +`(ny OR (new AND york)) city` that matches documents with the term `ny` or the conjunction `new AND york`. By default the parameter `auto_generate_synonyms_phrase_query` is set to `true`. From 23f12e42c13878b0ff3b4c33420eb1570ee586cd Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Wed, 12 Sep 2018 09:13:07 -0400 Subject: [PATCH 28/78] Expose CCR stats to monitoring (#33617) This commit exposes the CCR stats endpoint to monitoring collection. Co-authored-by: Martijn van Groningen --- .../multi-cluster-with-security/build.gradle | 2 +- .../xpack/ccr/FollowIndexSecurityIT.java | 44 ++++ .../plugin/ccr/qa/multi-cluster/build.gradle | 1 + .../xpack/ccr/FollowIndexIT.java | 36 +++ .../java/org/elasticsearch/xpack/ccr/Ccr.java | 16 +- .../elasticsearch/xpack/ccr/CcrSettings.java | 8 +- .../xpack/ccr/action/ShardFollowNodeTask.java | 1 + .../ccr/action/TransportCcrStatsAction.java | 32 +-- .../xpack/ccr/rest/RestCcrStatsAction.java | 2 +- .../elasticsearch/xpack/ccr/CcrLicenseIT.java | 4 +- .../xpack/core/XPackSettings.java | 6 + .../core/ccr/ShardFollowNodeTaskStatus.java | 95 ++++---- .../xpack/core/ccr/action/CcrStatsAction.java | 46 ++-- .../xpack/core/ccr/client/CcrClient.java | 8 +- .../xpack/monitoring/Monitoring.java | 3 + .../collector/ccr/CcrStatsCollector.java | 89 +++++++ .../collector/ccr/CcrStatsMonitoringDoc.java | 47 ++++ .../collector/ccr/CcrStatsCollectorTests.java | 217 ++++++++++++++++++ .../ccr/CcrStatsMonitoringDocTests.java | 175 ++++++++++++++ 19 files changed, 728 insertions(+), 104 deletions(-) create mode 100644 x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsCollector.java create mode 100644 x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsMonitoringDoc.java create mode 100644 x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsCollectorTests.java create mode 100644 x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsMonitoringDocTests.java diff --git a/x-pack/plugin/ccr/qa/multi-cluster-with-security/build.gradle b/x-pack/plugin/ccr/qa/multi-cluster-with-security/build.gradle index d4fe9ee554c..e2c772d7088 100644 --- a/x-pack/plugin/ccr/qa/multi-cluster-with-security/build.gradle +++ b/x-pack/plugin/ccr/qa/multi-cluster-with-security/build.gradle @@ -47,7 +47,7 @@ followClusterTestCluster { setting 'cluster.remote.leader_cluster.seeds', "\"${-> leaderClusterTest.nodes.get(0).transportUri()}\"" setting 'xpack.license.self_generated.type', 'trial' setting 'xpack.security.enabled', 'true' - setting 'xpack.monitoring.enabled', 'false' + setting 'xpack.monitoring.collection.enabled', 'true' extraConfigFile 'roles.yml', 'roles.yml' setupCommand 'setupTestAdmin', 'bin/elasticsearch-users', 'useradd', "test_admin", '-p', 'x-pack-test-password', '-r', "superuser" diff --git a/x-pack/plugin/ccr/qa/multi-cluster-with-security/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java b/x-pack/plugin/ccr/qa/multi-cluster-with-security/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java index d8357a74e8e..26389302ece 100644 --- a/x-pack/plugin/ccr/qa/multi-cluster-with-security/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java +++ b/x-pack/plugin/ccr/qa/multi-cluster-with-security/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java @@ -30,6 +30,7 @@ import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.is; public class FollowIndexSecurityIT extends ESRestTestCase { @@ -80,6 +81,7 @@ public class FollowIndexSecurityIT extends ESRestTestCase { createAndFollowIndex("leader_cluster:" + allowedIndex, allowedIndex); assertBusy(() -> verifyDocuments(client(), allowedIndex, numDocs)); assertThat(countCcrNodeTasks(), equalTo(1)); + assertBusy(() -> verifyCcrMonitoring(allowedIndex)); assertOK(client().performRequest(new Request("POST", "/" + allowedIndex + "/_ccr/unfollow"))); // Make sure that there are no other ccr relates operations running: assertBusy(() -> { @@ -203,4 +205,46 @@ public class FollowIndexSecurityIT extends ESRestTestCase { return RestStatus.OK.getStatus() == response.getStatusLine().getStatusCode(); } + private static void verifyCcrMonitoring(String expectedLeaderIndex) throws IOException { + ensureYellow(".monitoring-*"); + + Request request = new Request("GET", "/.monitoring-*/_search"); + request.setJsonEntity("{\"query\": {\"term\": {\"type\": \"ccr_stats\"}}}"); + Map response = toMap(adminClient().performRequest(request)); + + int numDocs = (int) XContentMapValues.extractValue("hits.total", response); + assertThat(numDocs, greaterThanOrEqualTo(1)); + + int numberOfOperationsReceived = 0; + int numberOfOperationsIndexed = 0; + + List hits = (List) XContentMapValues.extractValue("hits.hits", response); + for (int i = 0; i < numDocs; i++) { + Map hit = (Map) hits.get(i); + String leaderIndex = (String) XContentMapValues.extractValue("_source.ccr_stats.leader_index", hit); + if (leaderIndex.endsWith(expectedLeaderIndex) == false) { + continue; + } + + int foundNumberOfOperationsReceived = + (int) XContentMapValues.extractValue("_source.ccr_stats.operations_received", hit); + numberOfOperationsReceived = Math.max(numberOfOperationsReceived, foundNumberOfOperationsReceived); + int foundNumberOfOperationsIndexed = + (int) XContentMapValues.extractValue("_source.ccr_stats.number_of_operations_indexed", hit); + numberOfOperationsIndexed = Math.max(numberOfOperationsIndexed, foundNumberOfOperationsIndexed); + } + + assertThat(numberOfOperationsReceived, greaterThanOrEqualTo(1)); + assertThat(numberOfOperationsIndexed, greaterThanOrEqualTo(1)); + } + + private static void ensureYellow(String index) throws IOException { + Request request = new Request("GET", "/_cluster/health/" + index); + request.addParameter("wait_for_status", "yellow"); + request.addParameter("wait_for_no_relocating_shards", "true"); + request.addParameter("timeout", "70s"); + request.addParameter("level", "shards"); + adminClient().performRequest(request); + } + } diff --git a/x-pack/plugin/ccr/qa/multi-cluster/build.gradle b/x-pack/plugin/ccr/qa/multi-cluster/build.gradle index 396c247af40..b3b63723848 100644 --- a/x-pack/plugin/ccr/qa/multi-cluster/build.gradle +++ b/x-pack/plugin/ccr/qa/multi-cluster/build.gradle @@ -27,6 +27,7 @@ followClusterTestCluster { dependsOn leaderClusterTestRunner numNodes = 1 clusterName = 'follow-cluster' + setting 'xpack.monitoring.collection.enabled', 'true' setting 'xpack.license.self_generated.type', 'trial' setting 'cluster.remote.leader_cluster.seeds', "\"${-> leaderClusterTest.nodes.get(0).transportUri()}\"" } diff --git a/x-pack/plugin/ccr/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexIT.java b/x-pack/plugin/ccr/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexIT.java index 76d0e438135..0e56084e10c 100644 --- a/x-pack/plugin/ccr/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexIT.java +++ b/x-pack/plugin/ccr/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexIT.java @@ -25,6 +25,7 @@ import java.util.Map; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; public class FollowIndexIT extends ESRestTestCase { @@ -75,6 +76,7 @@ public class FollowIndexIT extends ESRestTestCase { index(leaderClient, leaderIndexName, Integer.toString(id + 2), "field", id + 2, "filtered_field", "true"); } assertBusy(() -> verifyDocuments(followIndexName, numDocs + 3)); + assertBusy(() -> verifyCcrMonitoring(leaderIndexName)); } } @@ -104,6 +106,7 @@ public class FollowIndexIT extends ESRestTestCase { ensureYellow("logs-20190101"); verifyDocuments("logs-20190101", 5); }); + assertBusy(() -> verifyCcrMonitoring("logs-20190101")); } private static void index(RestClient client, String index, String id, Object... fields) throws IOException { @@ -155,6 +158,39 @@ public class FollowIndexIT extends ESRestTestCase { } } + private static void verifyCcrMonitoring(String expectedLeaderIndex) throws IOException { + ensureYellow(".monitoring-*"); + + Request request = new Request("GET", "/.monitoring-*/_search"); + request.setJsonEntity("{\"query\": {\"term\": {\"type\": \"ccr_stats\"}}}"); + Map response = toMap(client().performRequest(request)); + + int numDocs = (int) XContentMapValues.extractValue("hits.total", response); + assertThat(numDocs, greaterThanOrEqualTo(1)); + + int numberOfOperationsReceived = 0; + int numberOfOperationsIndexed = 0; + + List hits = (List) XContentMapValues.extractValue("hits.hits", response); + for (int i = 0; i < numDocs; i++) { + Map hit = (Map) hits.get(i); + String leaderIndex = (String) XContentMapValues.extractValue("_source.ccr_stats.leader_index", hit); + if (leaderIndex.endsWith(expectedLeaderIndex) == false) { + continue; + } + + int foundNumberOfOperationsReceived = + (int) XContentMapValues.extractValue("_source.ccr_stats.operations_received", hit); + numberOfOperationsReceived = Math.max(numberOfOperationsReceived, foundNumberOfOperationsReceived); + int foundNumberOfOperationsIndexed = + (int) XContentMapValues.extractValue("_source.ccr_stats.number_of_operations_indexed", hit); + numberOfOperationsIndexed = Math.max(numberOfOperationsIndexed, foundNumberOfOperationsIndexed); + } + + assertThat(numberOfOperationsReceived, greaterThanOrEqualTo(1)); + assertThat(numberOfOperationsIndexed, greaterThanOrEqualTo(1)); + } + private static Map toMap(Response response) throws IOException { return toMap(EntityUtils.toString(response.getEntity())); } diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java index 39bed1ae771..72782f6e0fe 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java @@ -40,21 +40,17 @@ import org.elasticsearch.threadpool.FixedExecutorBuilder; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.ccr.action.AutoFollowCoordinator; -import org.elasticsearch.xpack.ccr.action.TransportUnfollowIndexAction; -import org.elasticsearch.xpack.core.ccr.action.CcrStatsAction; -import org.elasticsearch.xpack.ccr.action.TransportCreateAndFollowIndexAction; -import org.elasticsearch.xpack.ccr.action.TransportFollowIndexAction; -import org.elasticsearch.xpack.core.ccr.action.CreateAndFollowIndexAction; import org.elasticsearch.xpack.ccr.action.DeleteAutoFollowPatternAction; -import org.elasticsearch.xpack.core.ccr.action.FollowIndexAction; import org.elasticsearch.xpack.ccr.action.PutAutoFollowPatternAction; import org.elasticsearch.xpack.ccr.action.ShardChangesAction; import org.elasticsearch.xpack.ccr.action.ShardFollowTask; import org.elasticsearch.xpack.ccr.action.ShardFollowTasksExecutor; import org.elasticsearch.xpack.ccr.action.TransportCcrStatsAction; +import org.elasticsearch.xpack.ccr.action.TransportCreateAndFollowIndexAction; import org.elasticsearch.xpack.ccr.action.TransportDeleteAutoFollowPatternAction; +import org.elasticsearch.xpack.ccr.action.TransportFollowIndexAction; import org.elasticsearch.xpack.ccr.action.TransportPutAutoFollowPatternAction; -import org.elasticsearch.xpack.core.ccr.action.UnfollowIndexAction; +import org.elasticsearch.xpack.ccr.action.TransportUnfollowIndexAction; import org.elasticsearch.xpack.ccr.action.bulk.BulkShardOperationsAction; import org.elasticsearch.xpack.ccr.action.bulk.TransportBulkShardOperationsAction; import org.elasticsearch.xpack.ccr.index.engine.FollowingEngineFactory; @@ -66,6 +62,10 @@ import org.elasticsearch.xpack.ccr.rest.RestPutAutoFollowPatternAction; import org.elasticsearch.xpack.ccr.rest.RestUnfollowIndexAction; import org.elasticsearch.xpack.core.XPackPlugin; import org.elasticsearch.xpack.core.ccr.ShardFollowNodeTaskStatus; +import org.elasticsearch.xpack.core.ccr.action.CcrStatsAction; +import org.elasticsearch.xpack.core.ccr.action.CreateAndFollowIndexAction; +import org.elasticsearch.xpack.core.ccr.action.FollowIndexAction; +import org.elasticsearch.xpack.core.ccr.action.UnfollowIndexAction; import java.util.Arrays; import java.util.Collection; @@ -76,8 +76,8 @@ import java.util.Optional; import java.util.function.Supplier; import static java.util.Collections.emptyList; -import static org.elasticsearch.xpack.ccr.CcrSettings.CCR_ENABLED_SETTING; import static org.elasticsearch.xpack.ccr.CcrSettings.CCR_FOLLOWING_INDEX_SETTING; +import static org.elasticsearch.xpack.core.XPackSettings.CCR_ENABLED_SETTING; /** * Container class for CCR functionality. diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/CcrSettings.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/CcrSettings.java index a942990ea5a..122f5a913d2 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/CcrSettings.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/CcrSettings.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.ccr; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.xpack.core.XPackSettings; import java.util.Arrays; import java.util.List; @@ -22,11 +23,6 @@ public final class CcrSettings { } - /** - * Setting for controlling whether or not CCR is enabled. - */ - static final Setting CCR_ENABLED_SETTING = Setting.boolSetting("xpack.ccr.enabled", true, Property.NodeScope); - /** * Index setting for a following index. */ @@ -46,7 +42,7 @@ public final class CcrSettings { */ static List> getSettings() { return Arrays.asList( - CCR_ENABLED_SETTING, + XPackSettings.CCR_ENABLED_SETTING, CCR_FOLLOWING_INDEX_SETTING, CCR_AUTO_FOLLOW_POLL_INTERVAL); } diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTask.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTask.java index c221c097977..4237b89e967 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTask.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTask.java @@ -20,6 +20,7 @@ import org.elasticsearch.persistent.AllocatedPersistentTask; import org.elasticsearch.tasks.TaskId; import org.elasticsearch.xpack.ccr.action.bulk.BulkShardOperationsResponse; import org.elasticsearch.xpack.core.ccr.ShardFollowNodeTaskStatus; +import org.elasticsearch.xpack.core.ccr.action.FollowIndexAction; import java.util.ArrayList; import java.util.Arrays; diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportCcrStatsAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportCcrStatsAction.java index d4425773fa1..f227a56f158 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportCcrStatsAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportCcrStatsAction.java @@ -34,8 +34,8 @@ import java.util.function.Consumer; public class TransportCcrStatsAction extends TransportTasksAction< ShardFollowNodeTask, - CcrStatsAction.TasksRequest, - CcrStatsAction.TasksResponse, CcrStatsAction.TaskResponse> { + CcrStatsAction.StatsRequest, + CcrStatsAction.StatsResponses, CcrStatsAction.StatsResponse> { private final IndexNameExpressionResolver resolver; private final CcrLicenseChecker ccrLicenseChecker; @@ -54,8 +54,8 @@ public class TransportCcrStatsAction extends TransportTasksAction< clusterService, transportService, actionFilters, - CcrStatsAction.TasksRequest::new, - CcrStatsAction.TasksResponse::new, + CcrStatsAction.StatsRequest::new, + CcrStatsAction.StatsResponses::new, Ccr.CCR_THREAD_POOL_NAME); this.resolver = Objects.requireNonNull(resolver); this.ccrLicenseChecker = Objects.requireNonNull(ccrLicenseChecker); @@ -64,8 +64,8 @@ public class TransportCcrStatsAction extends TransportTasksAction< @Override protected void doExecute( final Task task, - final CcrStatsAction.TasksRequest request, - final ActionListener listener) { + final CcrStatsAction.StatsRequest request, + final ActionListener listener) { if (ccrLicenseChecker.isCcrAllowed() == false) { listener.onFailure(LicenseUtils.newComplianceException("ccr")); return; @@ -74,21 +74,21 @@ public class TransportCcrStatsAction extends TransportTasksAction< } @Override - protected CcrStatsAction.TasksResponse newResponse( - final CcrStatsAction.TasksRequest request, - final List taskResponses, + protected CcrStatsAction.StatsResponses newResponse( + final CcrStatsAction.StatsRequest request, + final List statsRespons, final List taskOperationFailures, final List failedNodeExceptions) { - return new CcrStatsAction.TasksResponse(taskOperationFailures, failedNodeExceptions, taskResponses); + return new CcrStatsAction.StatsResponses(taskOperationFailures, failedNodeExceptions, statsRespons); } @Override - protected CcrStatsAction.TaskResponse readTaskResponse(final StreamInput in) throws IOException { - return new CcrStatsAction.TaskResponse(in); + protected CcrStatsAction.StatsResponse readTaskResponse(final StreamInput in) throws IOException { + return new CcrStatsAction.StatsResponse(in); } @Override - protected void processTasks(final CcrStatsAction.TasksRequest request, final Consumer operation) { + protected void processTasks(final CcrStatsAction.StatsRequest request, final Consumer operation) { final ClusterState state = clusterService.state(); final Set concreteIndices = new HashSet<>(Arrays.asList(resolver.concreteIndexNames(state, request))); for (final Task task : taskManager.getTasks().values()) { @@ -103,10 +103,10 @@ public class TransportCcrStatsAction extends TransportTasksAction< @Override protected void taskOperation( - final CcrStatsAction.TasksRequest request, + final CcrStatsAction.StatsRequest request, final ShardFollowNodeTask task, - final ActionListener listener) { - listener.onResponse(new CcrStatsAction.TaskResponse(task.getFollowShardId(), task.getStatus())); + final ActionListener listener) { + listener.onResponse(new CcrStatsAction.StatsResponse(task.getFollowShardId(), task.getStatus())); } } diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestCcrStatsAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestCcrStatsAction.java index 0cf0aaf2e49..de285dba19e 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestCcrStatsAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestCcrStatsAction.java @@ -33,7 +33,7 @@ public class RestCcrStatsAction extends BaseRestHandler { @Override protected RestChannelConsumer prepareRequest(final RestRequest restRequest, final NodeClient client) throws IOException { - final CcrStatsAction.TasksRequest request = new CcrStatsAction.TasksRequest(); + final CcrStatsAction.StatsRequest request = new CcrStatsAction.StatsRequest(); request.setIndices(Strings.splitStringByCommaToArray(restRequest.param("index"))); request.setIndicesOptions(IndicesOptions.fromRequest(restRequest, request.indicesOptions())); return channel -> client.execute(CcrStatsAction.INSTANCE, request, new RestToXContentListener<>(channel)); diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/CcrLicenseIT.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/CcrLicenseIT.java index ecf2bd47fc7..f791be6a633 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/CcrLicenseIT.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/CcrLicenseIT.java @@ -90,9 +90,9 @@ public class CcrLicenseIT extends ESSingleNodeTestCase { public void testThatCcrStatsAreUnavailableWithNonCompliantLicense() throws InterruptedException { final CountDownLatch latch = new CountDownLatch(1); - client().execute(CcrStatsAction.INSTANCE, new CcrStatsAction.TasksRequest(), new ActionListener() { + client().execute(CcrStatsAction.INSTANCE, new CcrStatsAction.StatsRequest(), new ActionListener() { @Override - public void onResponse(final CcrStatsAction.TasksResponse tasksResponse) { + public void onResponse(final CcrStatsAction.StatsResponses statsResponses) { latch.countDown(); fail(); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackSettings.java index fb4ce0b90f4..997f04e33bd 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackSettings.java @@ -35,6 +35,12 @@ public class XPackSettings { throw new IllegalStateException("Utility class should not be instantiated"); } + + /** + * Setting for controlling whether or not CCR is enabled. + */ + public static final Setting CCR_ENABLED_SETTING = Setting.boolSetting("xpack.ccr.enabled", true, Property.NodeScope); + /** Setting for enabling or disabling security. Defaults to true. */ public static final Setting SECURITY_ENABLED = Setting.boolSetting("xpack.security.enabled", true, Setting.Property.NodeScope); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/ShardFollowNodeTaskStatus.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/ShardFollowNodeTaskStatus.java index 783999cf183..2f3c4efb9ad 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/ShardFollowNodeTaskStatus.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/ShardFollowNodeTaskStatus.java @@ -369,58 +369,63 @@ public class ShardFollowNodeTaskStatus implements Task.Status { public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException { builder.startObject(); { - builder.field(LEADER_INDEX.getPreferredName(), leaderIndex); - builder.field(SHARD_ID.getPreferredName(), shardId); - builder.field(LEADER_GLOBAL_CHECKPOINT_FIELD.getPreferredName(), leaderGlobalCheckpoint); - builder.field(LEADER_MAX_SEQ_NO_FIELD.getPreferredName(), leaderMaxSeqNo); - builder.field(FOLLOWER_GLOBAL_CHECKPOINT_FIELD.getPreferredName(), followerGlobalCheckpoint); - builder.field(FOLLOWER_MAX_SEQ_NO_FIELD.getPreferredName(), followerMaxSeqNo); - builder.field(LAST_REQUESTED_SEQ_NO_FIELD.getPreferredName(), lastRequestedSeqNo); - builder.field(NUMBER_OF_CONCURRENT_READS_FIELD.getPreferredName(), numberOfConcurrentReads); - builder.field(NUMBER_OF_CONCURRENT_WRITES_FIELD.getPreferredName(), numberOfConcurrentWrites); - builder.field(NUMBER_OF_QUEUED_WRITES_FIELD.getPreferredName(), numberOfQueuedWrites); - builder.field(MAPPING_VERSION_FIELD.getPreferredName(), mappingVersion); - builder.humanReadableField( - TOTAL_FETCH_TIME_MILLIS_FIELD.getPreferredName(), - "total_fetch_time", - new TimeValue(totalFetchTimeMillis, TimeUnit.MILLISECONDS)); - builder.field(NUMBER_OF_SUCCESSFUL_FETCHES_FIELD.getPreferredName(), numberOfSuccessfulFetches); - builder.field(NUMBER_OF_FAILED_FETCHES_FIELD.getPreferredName(), numberOfFailedFetches); - builder.field(OPERATIONS_RECEIVED_FIELD.getPreferredName(), operationsReceived); - builder.humanReadableField( - TOTAL_TRANSFERRED_BYTES.getPreferredName(), - "total_transferred", - new ByteSizeValue(totalTransferredBytes, ByteSizeUnit.BYTES)); - builder.humanReadableField( - TOTAL_INDEX_TIME_MILLIS_FIELD.getPreferredName(), - "total_index_time", - new TimeValue(totalIndexTimeMillis, TimeUnit.MILLISECONDS)); - builder.field(NUMBER_OF_SUCCESSFUL_BULK_OPERATIONS_FIELD.getPreferredName(), numberOfSuccessfulBulkOperations); - builder.field(NUMBER_OF_FAILED_BULK_OPERATIONS_FIELD.getPreferredName(), numberOfFailedBulkOperations); - builder.field(NUMBER_OF_OPERATIONS_INDEXED_FIELD.getPreferredName(), numberOfOperationsIndexed); - builder.startArray(FETCH_EXCEPTIONS.getPreferredName()); - { - for (final Map.Entry entry : fetchExceptions.entrySet()) { + toXContentFragment(builder, params); + } + builder.endObject(); + return builder; + } + + public XContentBuilder toXContentFragment(final XContentBuilder builder, final Params params) throws IOException { + builder.field(LEADER_INDEX.getPreferredName(), leaderIndex); + builder.field(SHARD_ID.getPreferredName(), shardId); + builder.field(LEADER_GLOBAL_CHECKPOINT_FIELD.getPreferredName(), leaderGlobalCheckpoint); + builder.field(LEADER_MAX_SEQ_NO_FIELD.getPreferredName(), leaderMaxSeqNo); + builder.field(FOLLOWER_GLOBAL_CHECKPOINT_FIELD.getPreferredName(), followerGlobalCheckpoint); + builder.field(FOLLOWER_MAX_SEQ_NO_FIELD.getPreferredName(), followerMaxSeqNo); + builder.field(LAST_REQUESTED_SEQ_NO_FIELD.getPreferredName(), lastRequestedSeqNo); + builder.field(NUMBER_OF_CONCURRENT_READS_FIELD.getPreferredName(), numberOfConcurrentReads); + builder.field(NUMBER_OF_CONCURRENT_WRITES_FIELD.getPreferredName(), numberOfConcurrentWrites); + builder.field(NUMBER_OF_QUEUED_WRITES_FIELD.getPreferredName(), numberOfQueuedWrites); + builder.field(MAPPING_VERSION_FIELD.getPreferredName(), mappingVersion); + builder.humanReadableField( + TOTAL_FETCH_TIME_MILLIS_FIELD.getPreferredName(), + "total_fetch_time", + new TimeValue(totalFetchTimeMillis, TimeUnit.MILLISECONDS)); + builder.field(NUMBER_OF_SUCCESSFUL_FETCHES_FIELD.getPreferredName(), numberOfSuccessfulFetches); + builder.field(NUMBER_OF_FAILED_FETCHES_FIELD.getPreferredName(), numberOfFailedFetches); + builder.field(OPERATIONS_RECEIVED_FIELD.getPreferredName(), operationsReceived); + builder.humanReadableField( + TOTAL_TRANSFERRED_BYTES.getPreferredName(), + "total_transferred", + new ByteSizeValue(totalTransferredBytes, ByteSizeUnit.BYTES)); + builder.humanReadableField( + TOTAL_INDEX_TIME_MILLIS_FIELD.getPreferredName(), + "total_index_time", + new TimeValue(totalIndexTimeMillis, TimeUnit.MILLISECONDS)); + builder.field(NUMBER_OF_SUCCESSFUL_BULK_OPERATIONS_FIELD.getPreferredName(), numberOfSuccessfulBulkOperations); + builder.field(NUMBER_OF_FAILED_BULK_OPERATIONS_FIELD.getPreferredName(), numberOfFailedBulkOperations); + builder.field(NUMBER_OF_OPERATIONS_INDEXED_FIELD.getPreferredName(), numberOfOperationsIndexed); + builder.startArray(FETCH_EXCEPTIONS.getPreferredName()); + { + for (final Map.Entry entry : fetchExceptions.entrySet()) { + builder.startObject(); + { + builder.field(FETCH_EXCEPTIONS_ENTRY_FROM_SEQ_NO.getPreferredName(), entry.getKey()); + builder.field(FETCH_EXCEPTIONS_ENTRY_EXCEPTION.getPreferredName()); builder.startObject(); { - builder.field(FETCH_EXCEPTIONS_ENTRY_FROM_SEQ_NO.getPreferredName(), entry.getKey()); - builder.field(FETCH_EXCEPTIONS_ENTRY_EXCEPTION.getPreferredName()); - builder.startObject(); - { - ElasticsearchException.generateThrowableXContent(builder, params, entry.getValue()); - } - builder.endObject(); + ElasticsearchException.generateThrowableXContent(builder, params, entry.getValue()); } builder.endObject(); } + builder.endObject(); } - builder.endArray(); - builder.humanReadableField( - TIME_SINCE_LAST_FETCH_MILLIS_FIELD.getPreferredName(), - "time_since_last_fetch", - new TimeValue(timeSinceLastFetchMillis, TimeUnit.MILLISECONDS)); } - builder.endObject(); + builder.endArray(); + builder.humanReadableField( + TIME_SINCE_LAST_FETCH_MILLIS_FIELD.getPreferredName(), + "time_since_last_fetch", + new TimeValue(timeSinceLastFetchMillis, TimeUnit.MILLISECONDS)); return builder; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/CcrStatsAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/CcrStatsAction.java index ace3d6bb194..1074b6905d3 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/CcrStatsAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/CcrStatsAction.java @@ -29,7 +29,7 @@ import java.util.List; import java.util.Map; import java.util.TreeMap; -public class CcrStatsAction extends Action { +public class CcrStatsAction extends Action { public static final String NAME = "cluster:monitor/ccr/stats"; @@ -40,41 +40,45 @@ public class CcrStatsAction extends Action { } @Override - public TasksResponse newResponse() { - return new TasksResponse(); + public StatsResponses newResponse() { + return new StatsResponses(); } - public static class TasksResponse extends BaseTasksResponse implements ToXContentObject { + public static class StatsResponses extends BaseTasksResponse implements ToXContentObject { - private final List taskResponses; + private final List statsResponse; - public TasksResponse() { + public List getStatsResponses() { + return statsResponse; + } + + public StatsResponses() { this(Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); } - public TasksResponse( + public StatsResponses( final List taskFailures, final List nodeFailures, - final List taskResponses) { + final List statsResponse) { super(taskFailures, nodeFailures); - this.taskResponses = taskResponses; + this.statsResponse = statsResponse; } @Override public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException { // sort by index name, then shard ID - final Map> taskResponsesByIndex = new TreeMap<>(); - for (final TaskResponse taskResponse : taskResponses) { + final Map> taskResponsesByIndex = new TreeMap<>(); + for (final StatsResponse statsResponse : statsResponse) { taskResponsesByIndex.computeIfAbsent( - taskResponse.followerShardId().getIndexName(), - k -> new TreeMap<>()).put(taskResponse.followerShardId().getId(), taskResponse); + statsResponse.followerShardId().getIndexName(), + k -> new TreeMap<>()).put(statsResponse.followerShardId().getId(), statsResponse); } builder.startObject(); { - for (final Map.Entry> index : taskResponsesByIndex.entrySet()) { + for (final Map.Entry> index : taskResponsesByIndex.entrySet()) { builder.startArray(index.getKey()); { - for (final Map.Entry shard : index.getValue().entrySet()) { + for (final Map.Entry shard : index.getValue().entrySet()) { shard.getValue().status().toXContent(builder, params); } } @@ -86,7 +90,7 @@ public class CcrStatsAction extends Action { } } - public static class TasksRequest extends BaseTasksRequest implements IndicesRequest { + public static class StatsRequest extends BaseTasksRequest implements IndicesRequest { private String[] indices; @@ -144,26 +148,26 @@ public class CcrStatsAction extends Action { } - public static class TaskResponse implements Writeable { + public static class StatsResponse implements Writeable { private final ShardId followerShardId; - ShardId followerShardId() { + public ShardId followerShardId() { return followerShardId; } private final ShardFollowNodeTaskStatus status; - ShardFollowNodeTaskStatus status() { + public ShardFollowNodeTaskStatus status() { return status; } - public TaskResponse(final ShardId followerShardId, final ShardFollowNodeTaskStatus status) { + public StatsResponse(final ShardId followerShardId, final ShardFollowNodeTaskStatus status) { this.followerShardId = followerShardId; this.status = status; } - public TaskResponse(final StreamInput in) throws IOException { + public StatsResponse(final StreamInput in) throws IOException { this.followerShardId = ShardId.readShardId(in); this.status = new ShardFollowNodeTaskStatus(in); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/client/CcrClient.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/client/CcrClient.java index e880b384824..881979e3d79 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/client/CcrClient.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/client/CcrClient.java @@ -49,13 +49,13 @@ public class CcrClient { } public void stats( - final CcrStatsAction.TasksRequest request, - final ActionListener listener) { + final CcrStatsAction.StatsRequest request, + final ActionListener listener) { client.execute(CcrStatsAction.INSTANCE, request, listener); } - public ActionFuture stats(final CcrStatsAction.TasksRequest request) { - final PlainActionFuture listener = PlainActionFuture.newFuture(); + public ActionFuture stats(final CcrStatsAction.StatsRequest request) { + final PlainActionFuture listener = PlainActionFuture.newFuture(); client.execute(CcrStatsAction.INSTANCE, request, listener); return listener; } diff --git a/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/Monitoring.java b/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/Monitoring.java index 4f9119df589..bb2ed76831d 100644 --- a/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/Monitoring.java +++ b/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/Monitoring.java @@ -39,6 +39,7 @@ import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.monitoring.action.TransportMonitoringBulkAction; import org.elasticsearch.xpack.monitoring.cleaner.CleanerService; import org.elasticsearch.xpack.monitoring.collector.Collector; +import org.elasticsearch.xpack.monitoring.collector.ccr.CcrStatsCollector; import org.elasticsearch.xpack.monitoring.collector.cluster.ClusterStatsCollector; import org.elasticsearch.xpack.monitoring.collector.indices.IndexRecoveryCollector; import org.elasticsearch.xpack.monitoring.collector.indices.IndexStatsCollector; @@ -142,6 +143,7 @@ public class Monitoring extends Plugin implements ActionPlugin { collectors.add(new NodeStatsCollector(settings, clusterService, getLicenseState(), client)); collectors.add(new IndexRecoveryCollector(settings, clusterService, getLicenseState(), client)); collectors.add(new JobStatsCollector(settings, clusterService, getLicenseState(), client)); + collectors.add(new CcrStatsCollector(settings, clusterService, getLicenseState(), client)); final MonitoringService monitoringService = new MonitoringService(settings, clusterService, threadPool, collectors, exporters); @@ -179,6 +181,7 @@ public class Monitoring extends Plugin implements ActionPlugin { settings.add(IndexRecoveryCollector.INDEX_RECOVERY_ACTIVE_ONLY); settings.add(IndexStatsCollector.INDEX_STATS_TIMEOUT); settings.add(JobStatsCollector.JOB_STATS_TIMEOUT); + settings.add(CcrStatsCollector.CCR_STATS_TIMEOUT); settings.add(NodeStatsCollector.NODE_STATS_TIMEOUT); settings.addAll(Exporters.getSettings()); return Collections.unmodifiableList(settings); diff --git a/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsCollector.java b/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsCollector.java new file mode 100644 index 00000000000..fbb7505af4d --- /dev/null +++ b/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsCollector.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.monitoring.collector.ccr; + +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.xpack.core.XPackClient; +import org.elasticsearch.xpack.core.XPackSettings; +import org.elasticsearch.xpack.core.ccr.action.CcrStatsAction; +import org.elasticsearch.xpack.core.ccr.client.CcrClient; +import org.elasticsearch.xpack.core.monitoring.exporter.MonitoringDoc; +import org.elasticsearch.xpack.monitoring.collector.Collector; + +import java.util.Collection; +import java.util.stream.Collectors; + +import static org.elasticsearch.xpack.core.ClientHelper.MONITORING_ORIGIN; +import static org.elasticsearch.xpack.core.ClientHelper.stashWithOrigin; +import static org.elasticsearch.xpack.monitoring.collector.ccr.CcrStatsMonitoringDoc.TYPE; + +public class CcrStatsCollector extends Collector { + + public static final Setting CCR_STATS_TIMEOUT = collectionTimeoutSetting("ccr.stats.timeout"); + + private final ThreadContext threadContext; + private final CcrClient ccrClient; + + public CcrStatsCollector( + final Settings settings, + final ClusterService clusterService, + final XPackLicenseState licenseState, + final Client client) { + this(settings, clusterService, licenseState, new XPackClient(client).ccr(), client.threadPool().getThreadContext()); + } + + CcrStatsCollector( + final Settings settings, + final ClusterService clusterService, + final XPackLicenseState licenseState, + final CcrClient ccrClient, + final ThreadContext threadContext) { + super(settings, TYPE, clusterService, CCR_STATS_TIMEOUT, licenseState); + this.ccrClient = ccrClient; + this.threadContext = threadContext; + } + + @Override + protected boolean shouldCollect(final boolean isElectedMaster) { + // this can only run when monitoring is allowed and CCR is enabled and allowed, but also only on the elected master node + return isElectedMaster + && super.shouldCollect(isElectedMaster) + && XPackSettings.CCR_ENABLED_SETTING.get(settings) + && licenseState.isCcrAllowed(); + } + + + @Override + protected Collection doCollect( + final MonitoringDoc.Node node, + final long interval, + final ClusterState clusterState) throws Exception { + try (ThreadContext.StoredContext ignore = stashWithOrigin(threadContext, MONITORING_ORIGIN)) { + final CcrStatsAction.StatsRequest request = new CcrStatsAction.StatsRequest(); + request.setIndices(Strings.EMPTY_ARRAY); + final CcrStatsAction.StatsResponses responses = ccrClient.stats(request).actionGet(getCollectionTimeout()); + + final long timestamp = timestamp(); + final String clusterUuid = clusterUuid(clusterState); + + return responses + .getStatsResponses() + .stream() + .map(stats -> new CcrStatsMonitoringDoc(clusterUuid, timestamp, interval, node, stats.status())) + .collect(Collectors.toList()); + } + } + +} diff --git a/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsMonitoringDoc.java b/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsMonitoringDoc.java new file mode 100644 index 00000000000..45c6a8607d4 --- /dev/null +++ b/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsMonitoringDoc.java @@ -0,0 +1,47 @@ +/* + * 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.monitoring.collector.ccr; + +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.xpack.core.ccr.ShardFollowNodeTaskStatus; +import org.elasticsearch.xpack.core.monitoring.MonitoredSystem; +import org.elasticsearch.xpack.core.monitoring.exporter.MonitoringDoc; + +import java.io.IOException; +import java.util.Objects; + +public class CcrStatsMonitoringDoc extends MonitoringDoc { + + public static final String TYPE = "ccr_stats"; + + private final ShardFollowNodeTaskStatus status; + + public ShardFollowNodeTaskStatus status() { + return status; + } + + public CcrStatsMonitoringDoc( + final String cluster, + final long timestamp, + final long intervalMillis, + final MonitoringDoc.Node node, + final ShardFollowNodeTaskStatus status) { + super(cluster, timestamp, intervalMillis, node, MonitoredSystem.ES, TYPE, null); + this.status = Objects.requireNonNull(status, "status"); + } + + + @Override + protected void innerToXContent(final XContentBuilder builder, final Params params) throws IOException { + builder.startObject(TYPE); + { + status.toXContentFragment(builder, params); + } + builder.endObject(); + } + +} diff --git a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsCollectorTests.java b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsCollectorTests.java new file mode 100644 index 00000000000..aaf3a61643b --- /dev/null +++ b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsCollectorTests.java @@ -0,0 +1,217 @@ +/* + * 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.monitoring.collector.ccr; + +import org.elasticsearch.action.ActionFuture; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.xpack.core.XPackSettings; +import org.elasticsearch.xpack.core.ccr.ShardFollowNodeTaskStatus; +import org.elasticsearch.xpack.core.ccr.action.CcrStatsAction; +import org.elasticsearch.xpack.core.ccr.client.CcrClient; +import org.elasticsearch.xpack.core.monitoring.MonitoredSystem; +import org.elasticsearch.xpack.core.monitoring.exporter.MonitoringDoc; +import org.elasticsearch.xpack.monitoring.BaseCollectorTestCase; +import org.mockito.ArgumentMatcher; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import static java.util.Collections.emptyList; +import static org.elasticsearch.xpack.monitoring.MonitoringTestUtils.randomMonitoringNode; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.mockito.Matchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class CcrStatsCollectorTests extends BaseCollectorTestCase { + + public void testShouldCollectReturnsFalseIfMonitoringNotAllowed() { + final Settings settings = randomFrom(ccrEnabledSettings(), ccrDisabledSettings()); + final boolean ccrAllowed = randomBoolean(); + final boolean isElectedMaster = randomBoolean(); + whenLocalNodeElectedMaster(isElectedMaster); + + // this controls the blockage + when(licenseState.isMonitoringAllowed()).thenReturn(false); + when(licenseState.isCcrAllowed()).thenReturn(ccrAllowed); + + final CcrStatsCollector collector = new CcrStatsCollector(settings, clusterService, licenseState, client); + + assertThat(collector.shouldCollect(isElectedMaster), is(false)); + if (isElectedMaster) { + verify(licenseState).isMonitoringAllowed(); + } + } + + public void testShouldCollectReturnsFalseIfNotMaster() { + // regardless of CCR being enabled + final Settings settings = randomFrom(ccrEnabledSettings(), ccrDisabledSettings()); + + when(licenseState.isMonitoringAllowed()).thenReturn(randomBoolean()); + when(licenseState.isCcrAllowed()).thenReturn(randomBoolean()); + // this controls the blockage + final boolean isElectedMaster = false; + + final CcrStatsCollector collector = new CcrStatsCollector(settings, clusterService, licenseState, client); + + assertThat(collector.shouldCollect(isElectedMaster), is(false)); + } + + public void testShouldCollectReturnsFalseIfCCRIsDisabled() { + // this is controls the blockage + final Settings settings = ccrDisabledSettings(); + + when(licenseState.isMonitoringAllowed()).thenReturn(randomBoolean()); + when(licenseState.isCcrAllowed()).thenReturn(randomBoolean()); + + final boolean isElectedMaster = randomBoolean(); + whenLocalNodeElectedMaster(isElectedMaster); + + final CcrStatsCollector collector = new CcrStatsCollector(settings, clusterService, licenseState, client); + + assertThat(collector.shouldCollect(isElectedMaster), is(false)); + + if (isElectedMaster) { + verify(licenseState).isMonitoringAllowed(); + } + } + + public void testShouldCollectReturnsFalseIfCCRIsNotAllowed() { + final Settings settings = randomFrom(ccrEnabledSettings(), ccrDisabledSettings()); + + when(licenseState.isMonitoringAllowed()).thenReturn(randomBoolean()); + // this is controls the blockage + when(licenseState.isCcrAllowed()).thenReturn(false); + final boolean isElectedMaster = randomBoolean(); + whenLocalNodeElectedMaster(isElectedMaster); + + final CcrStatsCollector collector = new CcrStatsCollector(settings, clusterService, licenseState, client); + + assertThat(collector.shouldCollect(isElectedMaster), is(false)); + + if (isElectedMaster) { + verify(licenseState).isMonitoringAllowed(); + } + } + + public void testShouldCollectReturnsTrue() { + final Settings settings = ccrEnabledSettings(); + + when(licenseState.isMonitoringAllowed()).thenReturn(true); + when(licenseState.isCcrAllowed()).thenReturn(true); + final boolean isElectedMaster = true; + + final CcrStatsCollector collector = new CcrStatsCollector(settings, clusterService, licenseState, client); + + assertThat(collector.shouldCollect(isElectedMaster), is(true)); + + verify(licenseState).isMonitoringAllowed(); + } + + public void testDoCollect() throws Exception { + final String clusterUuid = randomAlphaOfLength(5); + whenClusterStateWithUUID(clusterUuid); + + final MonitoringDoc.Node node = randomMonitoringNode(random()); + final CcrClient client = mock(CcrClient.class); + final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + + final TimeValue timeout = TimeValue.timeValueSeconds(randomIntBetween(1, 120)); + withCollectionTimeout(CcrStatsCollector.CCR_STATS_TIMEOUT, timeout); + + final CcrStatsCollector collector = new CcrStatsCollector(Settings.EMPTY, clusterService, licenseState, client, threadContext); + assertEquals(timeout, collector.getCollectionTimeout()); + + final List statuses = mockStatuses(); + + @SuppressWarnings("unchecked") + final ActionFuture future = (ActionFuture)mock(ActionFuture.class); + final CcrStatsAction.StatsResponses responses = new CcrStatsAction.StatsResponses(emptyList(), emptyList(), statuses); + + final CcrStatsAction.StatsRequest request = new CcrStatsAction.StatsRequest(); + request.setIndices(Strings.EMPTY_ARRAY); + when(client.stats(statsRequestEq(request))).thenReturn(future); + when(future.actionGet(timeout)).thenReturn(responses); + + final long interval = randomNonNegativeLong(); + + final Collection documents = collector.doCollect(node, interval, clusterState); + verify(clusterState).metaData(); + verify(metaData).clusterUUID(); + + assertThat(documents, hasSize(statuses.size())); + + int index = 0; + for (final Iterator it = documents.iterator(); it.hasNext(); index++) { + final CcrStatsMonitoringDoc document = (CcrStatsMonitoringDoc)it.next(); + final CcrStatsAction.StatsResponse status = statuses.get(index); + + assertThat(document.getCluster(), is(clusterUuid)); + assertThat(document.getTimestamp(), greaterThan(0L)); + assertThat(document.getIntervalMillis(), equalTo(interval)); + assertThat(document.getNode(), equalTo(node)); + assertThat(document.getSystem(), is(MonitoredSystem.ES)); + assertThat(document.getType(), is(CcrStatsMonitoringDoc.TYPE)); + assertThat(document.getId(), nullValue()); + assertThat(document.status(), is(status.status())); + } + } + + private List mockStatuses() { + final int count = randomIntBetween(1, 8); + final List statuses = new ArrayList<>(count); + + for (int i = 0; i < count; ++i) { + CcrStatsAction.StatsResponse statsResponse = mock(CcrStatsAction.StatsResponse.class); + ShardFollowNodeTaskStatus status = mock(ShardFollowNodeTaskStatus.class); + when(statsResponse.status()).thenReturn(status); + statuses.add(statsResponse); + } + + return statuses; + } + + private Settings ccrEnabledSettings() { + // since it's the default, we want to ensure we test both with/without it + return randomBoolean() ? Settings.EMPTY : Settings.builder().put(XPackSettings.CCR_ENABLED_SETTING.getKey(), true).build(); + } + + private Settings ccrDisabledSettings() { + return Settings.builder().put(XPackSettings.CCR_ENABLED_SETTING.getKey(), false).build(); + } + + private static CcrStatsAction.StatsRequest statsRequestEq(CcrStatsAction.StatsRequest expected) { + return argThat(new StatsRequestMatches(expected)); + } + + private static class StatsRequestMatches extends ArgumentMatcher { + + private final CcrStatsAction.StatsRequest expected; + + private StatsRequestMatches(CcrStatsAction.StatsRequest expected) { + this.expected = expected; + } + + @Override + public boolean matches(Object o) { + CcrStatsAction.StatsRequest actual = (CcrStatsAction.StatsRequest) o; + return Arrays.equals(expected.indices(), actual.indices()); + } + } + +} diff --git a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsMonitoringDocTests.java b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsMonitoringDocTests.java new file mode 100644 index 00000000000..47f2bdf5d2e --- /dev/null +++ b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsMonitoringDocTests.java @@ -0,0 +1,175 @@ +/* + * 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.monitoring.collector.ccr; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.xpack.core.ccr.ShardFollowNodeTaskStatus; +import org.elasticsearch.xpack.core.monitoring.MonitoredSystem; +import org.elasticsearch.xpack.core.monitoring.exporter.MonitoringDoc; +import org.elasticsearch.xpack.monitoring.exporter.BaseMonitoringDocTestCase; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.junit.Before; + +import java.io.IOException; +import java.util.Collections; +import java.util.NavigableMap; +import java.util.TreeMap; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasToString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.mockito.Mockito.mock; + +public class CcrStatsMonitoringDocTests extends BaseMonitoringDocTestCase { + + private ShardFollowNodeTaskStatus status; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + status = mock(ShardFollowNodeTaskStatus.class); + } + + public void testConstructorStatusMustNotBeNull() { + final NullPointerException e = + expectThrows(NullPointerException.class, () -> new CcrStatsMonitoringDoc(cluster, timestamp, interval, node, null)); + assertThat(e, hasToString(containsString("status"))); + } + + @Override + protected CcrStatsMonitoringDoc createMonitoringDoc( + final String cluster, + final long timestamp, + final long interval, + final MonitoringDoc.Node node, + final MonitoredSystem system, + final String type, + final String id) { + return new CcrStatsMonitoringDoc(cluster, timestamp, interval, node, status); + } + + @Override + protected void assertMonitoringDoc(CcrStatsMonitoringDoc document) { + assertThat(document.getSystem(), is(MonitoredSystem.ES)); + assertThat(document.getType(), is(CcrStatsMonitoringDoc.TYPE)); + assertThat(document.getId(), nullValue()); + assertThat(document.status(), is(status)); + } + + @Override + public void testToXContent() throws IOException { + final long timestamp = System.currentTimeMillis(); + final long intervalMillis = System.currentTimeMillis(); + final long nodeTimestamp = System.currentTimeMillis(); + final MonitoringDoc.Node node = new MonitoringDoc.Node("_uuid", "_host", "_addr", "_ip", "_name", nodeTimestamp); + // these random values do not need to be internally consistent, they are only for testing formatting + final int shardId = randomIntBetween(0, Integer.MAX_VALUE); + final long leaderGlobalCheckpoint = randomNonNegativeLong(); + final long leaderMaxSeqNo = randomNonNegativeLong(); + final long followerGlobalCheckpoint = randomNonNegativeLong(); + final long followerMaxSeqNo = randomNonNegativeLong(); + final long lastRequestedSeqNo = randomNonNegativeLong(); + final int numberOfConcurrentReads = randomIntBetween(1, Integer.MAX_VALUE); + final int numberOfConcurrentWrites = randomIntBetween(1, Integer.MAX_VALUE); + final int numberOfQueuedWrites = randomIntBetween(0, Integer.MAX_VALUE); + final long mappingVersion = randomIntBetween(0, Integer.MAX_VALUE); + final long totalFetchTimeMillis = randomLongBetween(0, 4096); + final long numberOfSuccessfulFetches = randomNonNegativeLong(); + final long numberOfFailedFetches = randomLongBetween(0, 8); + final long operationsReceived = randomNonNegativeLong(); + final long totalTransferredBytes = randomNonNegativeLong(); + final long totalIndexTimeMillis = randomNonNegativeLong(); + final long numberOfSuccessfulBulkOperations = randomNonNegativeLong(); + final long numberOfFailedBulkOperations = randomNonNegativeLong(); + final long numberOfOperationsIndexed = randomNonNegativeLong(); + final NavigableMap fetchExceptions = + new TreeMap<>(Collections.singletonMap(randomNonNegativeLong(), new ElasticsearchException("shard is sad"))); + final long timeSinceLastFetchMillis = randomNonNegativeLong(); + final ShardFollowNodeTaskStatus status = new ShardFollowNodeTaskStatus( + "cluster_alias:leader_index", + shardId, + leaderGlobalCheckpoint, + leaderMaxSeqNo, + followerGlobalCheckpoint, + followerMaxSeqNo, + lastRequestedSeqNo, + numberOfConcurrentReads, + numberOfConcurrentWrites, + numberOfQueuedWrites, + mappingVersion, + totalFetchTimeMillis, + numberOfSuccessfulFetches, + numberOfFailedFetches, + operationsReceived, + totalTransferredBytes, + totalIndexTimeMillis, + numberOfSuccessfulBulkOperations, + numberOfFailedBulkOperations, + numberOfOperationsIndexed, + fetchExceptions, + timeSinceLastFetchMillis); + final CcrStatsMonitoringDoc document = new CcrStatsMonitoringDoc("_cluster", timestamp, intervalMillis, node, status); + final BytesReference xContent = XContentHelper.toXContent(document, XContentType.JSON, false); + assertThat( + xContent.utf8ToString(), + equalTo( + "{" + + "\"cluster_uuid\":\"_cluster\"," + + "\"timestamp\":\"" + new DateTime(timestamp, DateTimeZone.UTC).toString() + "\"," + + "\"interval_ms\":" + intervalMillis + "," + + "\"type\":\"ccr_stats\"," + + "\"source_node\":{" + + "\"uuid\":\"_uuid\"," + + "\"host\":\"_host\"," + + "\"transport_address\":\"_addr\"," + + "\"ip\":\"_ip\"," + + "\"name\":\"_name\"," + + "\"timestamp\":\"" + new DateTime(nodeTimestamp, DateTimeZone.UTC).toString() + "\"" + + "}," + + "\"ccr_stats\":{" + + "\"leader_index\":\"cluster_alias:leader_index\"," + + "\"shard_id\":" + shardId + "," + + "\"leader_global_checkpoint\":" + leaderGlobalCheckpoint + "," + + "\"leader_max_seq_no\":" + leaderMaxSeqNo + "," + + "\"follower_global_checkpoint\":" + followerGlobalCheckpoint + "," + + "\"follower_max_seq_no\":" + followerMaxSeqNo + "," + + "\"last_requested_seq_no\":" + lastRequestedSeqNo + "," + + "\"number_of_concurrent_reads\":" + numberOfConcurrentReads + "," + + "\"number_of_concurrent_writes\":" + numberOfConcurrentWrites + "," + + "\"number_of_queued_writes\":" + numberOfQueuedWrites + "," + + "\"mapping_version\":" + mappingVersion + "," + + "\"total_fetch_time_millis\":" + totalFetchTimeMillis + "," + + "\"number_of_successful_fetches\":" + numberOfSuccessfulFetches + "," + + "\"number_of_failed_fetches\":" + numberOfFailedFetches + "," + + "\"operations_received\":" + operationsReceived + "," + + "\"total_transferred_bytes\":" + totalTransferredBytes + "," + + "\"total_index_time_millis\":" + totalIndexTimeMillis +"," + + "\"number_of_successful_bulk_operations\":" + numberOfSuccessfulBulkOperations + "," + + "\"number_of_failed_bulk_operations\":" + numberOfFailedBulkOperations + "," + + "\"number_of_operations_indexed\":" + numberOfOperationsIndexed + "," + + "\"fetch_exceptions\":[" + + "{" + + "\"from_seq_no\":" + fetchExceptions.keySet().iterator().next() + "," + + "\"exception\":{" + + "\"type\":\"exception\"," + + "\"reason\":\"shard is sad\"" + + "}" + + "}" + + "]," + + "\"time_since_last_fetch_millis\":" + timeSinceLastFetchMillis + + "}" + + "}")); + } + +} From 7e195c2912b9ed9fad3099761562dd0a7731635c Mon Sep 17 00:00:00 2001 From: Tanguy Leroux Date: Wed, 12 Sep 2018 15:27:57 +0200 Subject: [PATCH 29/78] Update AWS SDK to 1.11.406 in repository-s3 (#30723) --- plugins/repository-s3/build.gradle | 7 +++- .../aws-java-sdk-core-1.11.223.jar.sha1 | 1 - .../aws-java-sdk-core-1.11.406.jar.sha1 | 1 + .../aws-java-sdk-kms-1.11.223.jar.sha1 | 1 - .../aws-java-sdk-kms-1.11.406.jar.sha1 | 1 + .../aws-java-sdk-s3-1.11.223.jar.sha1 | 1 - .../aws-java-sdk-s3-1.11.406.jar.sha1 | 1 + .../licenses/jmespath-java-1.11.406.jar.sha1 | 1 + .../repositories/s3/S3Service.java | 35 ++++++++++++------- .../s3/RepositoryCredentialsTests.java | 7 ++-- 10 files changed, 35 insertions(+), 21 deletions(-) delete mode 100644 plugins/repository-s3/licenses/aws-java-sdk-core-1.11.223.jar.sha1 create mode 100644 plugins/repository-s3/licenses/aws-java-sdk-core-1.11.406.jar.sha1 delete mode 100644 plugins/repository-s3/licenses/aws-java-sdk-kms-1.11.223.jar.sha1 create mode 100644 plugins/repository-s3/licenses/aws-java-sdk-kms-1.11.406.jar.sha1 delete mode 100644 plugins/repository-s3/licenses/aws-java-sdk-s3-1.11.223.jar.sha1 create mode 100644 plugins/repository-s3/licenses/aws-java-sdk-s3-1.11.406.jar.sha1 create mode 100644 plugins/repository-s3/licenses/jmespath-java-1.11.406.jar.sha1 diff --git a/plugins/repository-s3/build.gradle b/plugins/repository-s3/build.gradle index 5d248b22caf..c56a9a8259a 100644 --- a/plugins/repository-s3/build.gradle +++ b/plugins/repository-s3/build.gradle @@ -32,19 +32,23 @@ esplugin { } versions << [ - 'aws': '1.11.223' + 'aws': '1.11.406' ] dependencies { compile "com.amazonaws:aws-java-sdk-s3:${versions.aws}" compile "com.amazonaws:aws-java-sdk-kms:${versions.aws}" compile "com.amazonaws:aws-java-sdk-core:${versions.aws}" + compile "com.amazonaws:jmespath-java:${versions.aws}" compile "org.apache.httpcomponents:httpclient:${versions.httpclient}" compile "org.apache.httpcomponents:httpcore:${versions.httpcore}" compile "commons-logging:commons-logging:${versions.commonslogging}" compile "commons-codec:commons-codec:${versions.commonscodec}" + compile "com.fasterxml.jackson.core:jackson-core:${versions.jackson}" compile 'com.fasterxml.jackson.core:jackson-databind:2.6.7.1' compile 'com.fasterxml.jackson.core:jackson-annotations:2.6.0' + compile "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:${versions.jackson}" + compile 'joda-time:joda-time:2.10' // HACK: javax.xml.bind was removed from default modules in java 9, so we pull the api in here, // and whitelist this hack in JarHell @@ -53,6 +57,7 @@ dependencies { dependencyLicenses { mapping from: /aws-java-sdk-.*/, to: 'aws-java-sdk' + mapping from: /jmespath-java.*/, to: 'aws-java-sdk' mapping from: /jackson-.*/, to: 'jackson' mapping from: /jaxb-.*/, to: 'jaxb' } diff --git a/plugins/repository-s3/licenses/aws-java-sdk-core-1.11.223.jar.sha1 b/plugins/repository-s3/licenses/aws-java-sdk-core-1.11.223.jar.sha1 deleted file mode 100644 index 9890dd8d600..00000000000 --- a/plugins/repository-s3/licenses/aws-java-sdk-core-1.11.223.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -c3993cb44f5856fa721b7b7ccfc266377c0bf9c0 \ No newline at end of file diff --git a/plugins/repository-s3/licenses/aws-java-sdk-core-1.11.406.jar.sha1 b/plugins/repository-s3/licenses/aws-java-sdk-core-1.11.406.jar.sha1 new file mode 100644 index 00000000000..415373b275e --- /dev/null +++ b/plugins/repository-s3/licenses/aws-java-sdk-core-1.11.406.jar.sha1 @@ -0,0 +1 @@ +43f3b7332d4d527bbf34d4ac6be094f3dabec6de \ No newline at end of file diff --git a/plugins/repository-s3/licenses/aws-java-sdk-kms-1.11.223.jar.sha1 b/plugins/repository-s3/licenses/aws-java-sdk-kms-1.11.223.jar.sha1 deleted file mode 100644 index d5bc9d30308..00000000000 --- a/plugins/repository-s3/licenses/aws-java-sdk-kms-1.11.223.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -c24e6ebe108c60a08098aeaad5ae0b6a5a77b618 \ No newline at end of file diff --git a/plugins/repository-s3/licenses/aws-java-sdk-kms-1.11.406.jar.sha1 b/plugins/repository-s3/licenses/aws-java-sdk-kms-1.11.406.jar.sha1 new file mode 100644 index 00000000000..f0eb9b71752 --- /dev/null +++ b/plugins/repository-s3/licenses/aws-java-sdk-kms-1.11.406.jar.sha1 @@ -0,0 +1 @@ +e29854e58dc20f5453c1da7e580a5921b1e9714a \ No newline at end of file diff --git a/plugins/repository-s3/licenses/aws-java-sdk-s3-1.11.223.jar.sha1 b/plugins/repository-s3/licenses/aws-java-sdk-s3-1.11.223.jar.sha1 deleted file mode 100644 index fe12b2d4847..00000000000 --- a/plugins/repository-s3/licenses/aws-java-sdk-s3-1.11.223.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -c2ef96732e22d97952fbcd0a94f1dc376d157eda \ No newline at end of file diff --git a/plugins/repository-s3/licenses/aws-java-sdk-s3-1.11.406.jar.sha1 b/plugins/repository-s3/licenses/aws-java-sdk-s3-1.11.406.jar.sha1 new file mode 100644 index 00000000000..e57fd11c829 --- /dev/null +++ b/plugins/repository-s3/licenses/aws-java-sdk-s3-1.11.406.jar.sha1 @@ -0,0 +1 @@ +5c3c2c57b076602b3aeef841c63e5848ec52b00d \ No newline at end of file diff --git a/plugins/repository-s3/licenses/jmespath-java-1.11.406.jar.sha1 b/plugins/repository-s3/licenses/jmespath-java-1.11.406.jar.sha1 new file mode 100644 index 00000000000..bbb9b562a2f --- /dev/null +++ b/plugins/repository-s3/licenses/jmespath-java-1.11.406.jar.sha1 @@ -0,0 +1 @@ +06c291d1029943d4968a36fadffa3b71a6d8b4e4 \ No newline at end of file diff --git a/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3Service.java b/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3Service.java index b177686bd71..a431f4da1fd 100644 --- a/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3Service.java +++ b/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3Service.java @@ -23,10 +23,12 @@ import com.amazonaws.ClientConfiguration; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.auth.EC2ContainerCredentialsProviderWrapper; +import com.amazonaws.client.builder.AwsClientBuilder; import com.amazonaws.http.IdleConnectionReaper; import com.amazonaws.internal.StaticCredentialsProvider; import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import com.amazonaws.services.s3.internal.Constants; import org.apache.logging.log4j.Logger; import org.elasticsearch.common.Strings; import org.elasticsearch.common.collect.MapBuilder; @@ -93,19 +95,26 @@ class S3Service extends AbstractComponent implements Closeable { } } - private AmazonS3 buildClient(S3ClientSettings clientSettings) { - final AWSCredentialsProvider credentials = buildCredentials(logger, clientSettings); - final ClientConfiguration configuration = buildConfiguration(clientSettings); - final AmazonS3 client = buildClient(credentials, configuration); - if (Strings.hasText(clientSettings.endpoint)) { - client.setEndpoint(clientSettings.endpoint); - } - return client; - } - // proxy for testing - AmazonS3 buildClient(AWSCredentialsProvider credentials, ClientConfiguration configuration) { - return new AmazonS3Client(credentials, configuration); + AmazonS3 buildClient(final S3ClientSettings clientSettings) { + final AmazonS3ClientBuilder builder = AmazonS3ClientBuilder.standard(); + builder.withCredentials(buildCredentials(logger, clientSettings)); + builder.withClientConfiguration(buildConfiguration(clientSettings)); + + final String endpoint = Strings.hasLength(clientSettings.endpoint) ? clientSettings.endpoint : Constants.S3_HOSTNAME; + logger.debug("using endpoint [{}]", endpoint); + + // If the endpoint configuration isn't set on the builder then the default behaviour is to try + // and work out what region we are in and use an appropriate endpoint - see AwsClientBuilder#setRegion. + // In contrast, directly-constructed clients use s3.amazonaws.com unless otherwise instructed. We currently + // use a directly-constructed client, and need to keep the existing behaviour to avoid a breaking change, + // so to move to using the builder we must set it explicitly to keep the existing behaviour. + // + // We do this because directly constructing the client is deprecated (was already deprecated in 1.1.223 too) + // so this change removes that usage of a deprecated API. + builder.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endpoint, null)); + + return builder.build(); } // pkg private for tests diff --git a/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/RepositoryCredentialsTests.java b/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/RepositoryCredentialsTests.java index 7eb603b4b78..17797a57583 100644 --- a/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/RepositoryCredentialsTests.java +++ b/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/RepositoryCredentialsTests.java @@ -19,7 +19,6 @@ package org.elasticsearch.repositories.s3; -import com.amazonaws.ClientConfiguration; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.services.s3.AmazonS3; @@ -70,9 +69,9 @@ public class RepositoryCredentialsTests extends ESTestCase { } @Override - AmazonS3 buildClient(AWSCredentialsProvider credentials, ClientConfiguration configuration) { - final AmazonS3 client = super.buildClient(credentials, configuration); - return new ClientAndCredentials(client, credentials); + AmazonS3 buildClient(final S3ClientSettings clientSettings) { + final AmazonS3 client = super.buildClient(clientSettings); + return new ClientAndCredentials(client, buildCredentials(logger, clientSettings)); } } From 2eb2313b6031da8b933c95e785545938c63eface Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Wed, 12 Sep 2018 14:52:36 +0100 Subject: [PATCH 30/78] [HLRC][ML] Add ML put datafeed API to HLRC (#33603) This also changes both `DatafeedConfig` and `DatafeedUpdate` to store the query and aggs as a bytes reference. This allows the client to remove its dependency to the named objects registry of the search module. Relates #29827 --- .../client/MLRequestConverters.java | 13 ++ .../client/MachineLearningClient.java | 53 ++++++- .../client/ml/PutDatafeedRequest.java | 84 +++++++++++ .../client/ml/PutDatafeedResponse.java | 70 +++++++++ .../client/ml/datafeed/DatafeedConfig.java | 142 ++++++++++++------ .../client/ml/datafeed/DatafeedUpdate.java | 98 +++++++++--- .../client/MLRequestConvertersTests.java | 17 +++ .../client/MachineLearningIT.java | 48 ++++-- .../MlClientDocumentationIT.java | 112 +++++++++++++- .../client/ml/PutDatafeedRequestTests.java | 43 ++++++ .../client/ml/PutDatafeedResponseTests.java | 49 ++++++ .../ml/datafeed/DatafeedConfigTests.java | 40 ++--- .../ml/datafeed/DatafeedUpdateTests.java | 24 ++- .../high-level/ml/put-datafeed.asciidoc | 124 +++++++++++++++ docs/java-rest/high-level/ml/put-job.asciidoc | 2 +- .../high-level/supported-apis.asciidoc | 2 + 16 files changed, 793 insertions(+), 128 deletions(-) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PutDatafeedRequest.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PutDatafeedResponse.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PutDatafeedRequestTests.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PutDatafeedResponseTests.java create mode 100644 docs/java-rest/high-level/ml/put-datafeed.asciidoc diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java index 9504c394c69..09c587cf81f 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java @@ -41,6 +41,7 @@ import org.elasticsearch.client.ml.GetOverallBucketsRequest; import org.elasticsearch.client.ml.GetRecordsRequest; import org.elasticsearch.client.ml.OpenJobRequest; import org.elasticsearch.client.ml.PostDataRequest; +import org.elasticsearch.client.ml.PutDatafeedRequest; import org.elasticsearch.client.ml.PutJobRequest; import org.elasticsearch.client.ml.UpdateJobRequest; import org.elasticsearch.common.Strings; @@ -182,6 +183,18 @@ final class MLRequestConverters { return request; } + static Request putDatafeed(PutDatafeedRequest putDatafeedRequest) throws IOException { + String endpoint = new EndpointBuilder() + .addPathPartAsIs("_xpack") + .addPathPartAsIs("ml") + .addPathPartAsIs("datafeeds") + .addPathPart(putDatafeedRequest.getDatafeed().getId()) + .build(); + Request request = new Request(HttpPut.METHOD_NAME, endpoint); + request.setEntity(createEntity(putDatafeedRequest, REQUEST_BODY_CONTENT_TYPE)); + return request; + } + static Request deleteForecast(DeleteForecastRequest deleteForecastRequest) throws IOException { String endpoint = new EndpointBuilder() .addPathPartAsIs("_xpack") diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java index d42d2b58d44..79f9267c94d 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java @@ -20,18 +20,15 @@ package org.elasticsearch.client; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.master.AcknowledgedResponse; -import org.elasticsearch.client.ml.DeleteForecastRequest; -import org.elasticsearch.client.ml.ForecastJobRequest; -import org.elasticsearch.client.ml.ForecastJobResponse; -import org.elasticsearch.client.ml.PostDataRequest; -import org.elasticsearch.client.ml.PostDataResponse; -import org.elasticsearch.client.ml.UpdateJobRequest; import org.elasticsearch.client.ml.CloseJobRequest; import org.elasticsearch.client.ml.CloseJobResponse; +import org.elasticsearch.client.ml.DeleteForecastRequest; import org.elasticsearch.client.ml.DeleteJobRequest; import org.elasticsearch.client.ml.DeleteJobResponse; import org.elasticsearch.client.ml.FlushJobRequest; import org.elasticsearch.client.ml.FlushJobResponse; +import org.elasticsearch.client.ml.ForecastJobRequest; +import org.elasticsearch.client.ml.ForecastJobResponse; import org.elasticsearch.client.ml.GetBucketsRequest; import org.elasticsearch.client.ml.GetBucketsResponse; import org.elasticsearch.client.ml.GetCategoriesRequest; @@ -48,13 +45,19 @@ import org.elasticsearch.client.ml.GetRecordsRequest; import org.elasticsearch.client.ml.GetRecordsResponse; import org.elasticsearch.client.ml.OpenJobRequest; import org.elasticsearch.client.ml.OpenJobResponse; +import org.elasticsearch.client.ml.PostDataRequest; +import org.elasticsearch.client.ml.PostDataResponse; +import org.elasticsearch.client.ml.PutDatafeedRequest; +import org.elasticsearch.client.ml.PutDatafeedResponse; import org.elasticsearch.client.ml.PutJobRequest; import org.elasticsearch.client.ml.PutJobResponse; +import org.elasticsearch.client.ml.UpdateJobRequest; import org.elasticsearch.client.ml.job.stats.JobStats; import java.io.IOException; import java.util.Collections; + /** * Machine Learning API client wrapper for the {@link RestHighLevelClient} * @@ -451,6 +454,44 @@ public final class MachineLearningClient { Collections.emptySet()); } + /** + * Creates a new Machine Learning Datafeed + *

+ * For additional info + * see ML PUT datafeed documentation + * + * @param request The PutDatafeedRequest containing the {@link org.elasticsearch.client.ml.datafeed.DatafeedConfig} settings + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return PutDatafeedResponse with enclosed {@link org.elasticsearch.client.ml.datafeed.DatafeedConfig} object + * @throws IOException when there is a serialization issue sending the request or receiving the response + */ + public PutDatafeedResponse putDatafeed(PutDatafeedRequest request, RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(request, + MLRequestConverters::putDatafeed, + options, + PutDatafeedResponse::fromXContent, + Collections.emptySet()); + } + + /** + * Creates a new Machine Learning Datafeed asynchronously and notifies listener on completion + *

+ * For additional info + * see ML PUT datafeed documentation + * + * @param request The request containing the {@link org.elasticsearch.client.ml.datafeed.DatafeedConfig} settings + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener Listener to be notified upon request completion + */ + public void putDatafeedAsync(PutDatafeedRequest request, RequestOptions options, ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(request, + MLRequestConverters::putDatafeed, + options, + PutDatafeedResponse::fromXContent, + listener, + Collections.emptySet()); + } + /** * Deletes Machine Learning Job Forecasts * diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PutDatafeedRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PutDatafeedRequest.java new file mode 100644 index 00000000000..34cb12599a6 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PutDatafeedRequest.java @@ -0,0 +1,84 @@ +/* + * 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; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.client.ml.datafeed.DatafeedConfig; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Objects; + +/** + * Request to create a new Machine Learning Datafeed given a {@link DatafeedConfig} configuration + */ +public class PutDatafeedRequest extends ActionRequest implements ToXContentObject { + + private final DatafeedConfig datafeed; + + /** + * Construct a new PutDatafeedRequest + * + * @param datafeed a {@link DatafeedConfig} configuration to create + */ + public PutDatafeedRequest(DatafeedConfig datafeed) { + this.datafeed = datafeed; + } + + public DatafeedConfig getDatafeed() { + return datafeed; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return datafeed.toXContent(builder, params); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + PutDatafeedRequest request = (PutDatafeedRequest) object; + return Objects.equals(datafeed, request.datafeed); + } + + @Override + public int hashCode() { + return Objects.hash(datafeed); + } + + @Override + public final String toString() { + return Strings.toString(this); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PutDatafeedResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PutDatafeedResponse.java new file mode 100644 index 00000000000..fa9862fd3b9 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PutDatafeedResponse.java @@ -0,0 +1,70 @@ +/* + * 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; + +import org.elasticsearch.client.ml.datafeed.DatafeedConfig; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Objects; + +/** + * Response containing the newly created {@link DatafeedConfig} + */ +public class PutDatafeedResponse implements ToXContentObject { + + private DatafeedConfig datafeed; + + public static PutDatafeedResponse fromXContent(XContentParser parser) throws IOException { + return new PutDatafeedResponse(DatafeedConfig.PARSER.parse(parser, null).build()); + } + + PutDatafeedResponse(DatafeedConfig datafeed) { + this.datafeed = datafeed; + } + + public DatafeedConfig getResponse() { + return datafeed; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + datafeed.toXContent(builder, params); + return builder; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } + PutDatafeedResponse response = (PutDatafeedResponse) object; + return Objects.equals(datafeed, response.datafeed); + } + + @Override + public int hashCode() { + return Objects.hash(datafeed); + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/datafeed/DatafeedConfig.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/datafeed/DatafeedConfig.java index 752752b1038..84deae61f8e 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/datafeed/DatafeedConfig.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/datafeed/DatafeedConfig.java @@ -20,36 +20,37 @@ package org.elasticsearch.client.ml.datafeed; import org.elasticsearch.client.ml.job.config.Job; import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.index.query.AbstractQueryBuilder; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.index.query.QueryBuilder; -import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.aggregations.AggregatorFactories; import org.elasticsearch.search.builder.SearchSourceBuilder; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Map; import java.util.Objects; /** - * Datafeed configuration options pojo. Describes where to proactively pull input - * data from. - *

- * If a value has not been set it will be null. Object wrappers are - * used around integral types and booleans so they can take null - * values. + * The datafeed configuration object. It specifies which indices + * to get the data from and offers parameters for customizing different + * aspects of the process. */ public class DatafeedConfig implements ToXContentObject { - public static final int DEFAULT_SCROLL_SIZE = 1000; - public static final ParseField ID = new ParseField("datafeed_id"); public static final ParseField QUERY_DELAY = new ParseField("query_delay"); public static final ParseField FREQUENCY = new ParseField("frequency"); @@ -59,7 +60,6 @@ public class DatafeedConfig implements ToXContentObject { public static final ParseField QUERY = new ParseField("query"); public static final ParseField SCROLL_SIZE = new ParseField("scroll_size"); public static final ParseField AGGREGATIONS = new ParseField("aggregations"); - public static final ParseField AGGS = new ParseField("aggs"); public static final ParseField SCRIPT_FIELDS = new ParseField("script_fields"); public static final ParseField CHUNKING_CONFIG = new ParseField("chunking_config"); @@ -77,9 +77,8 @@ public class DatafeedConfig implements ToXContentObject { builder.setQueryDelay(TimeValue.parseTimeValue(val, QUERY_DELAY.getPreferredName())), QUERY_DELAY); PARSER.declareString((builder, val) -> builder.setFrequency(TimeValue.parseTimeValue(val, FREQUENCY.getPreferredName())), FREQUENCY); - PARSER.declareObject(Builder::setQuery, (p, c) -> AbstractQueryBuilder.parseInnerQueryBuilder(p), QUERY); - PARSER.declareObject(Builder::setAggregations, (p, c) -> AggregatorFactories.parseAggregators(p), AGGREGATIONS); - PARSER.declareObject(Builder::setAggregations, (p, c) -> AggregatorFactories.parseAggregators(p), AGGS); + PARSER.declareField(Builder::setQuery, DatafeedConfig::parseBytes, QUERY, ObjectParser.ValueType.OBJECT); + PARSER.declareField(Builder::setAggregations, DatafeedConfig::parseBytes, AGGREGATIONS, ObjectParser.ValueType.OBJECT); PARSER.declareObject(Builder::setScriptFields, (p, c) -> { List parsedScriptFields = new ArrayList<>(); while (p.nextToken() != XContentParser.Token.END_OBJECT) { @@ -91,29 +90,26 @@ public class DatafeedConfig implements ToXContentObject { PARSER.declareObject(Builder::setChunkingConfig, ChunkingConfig.PARSER, CHUNKING_CONFIG); } + private static BytesReference parseBytes(XContentParser parser) throws IOException { + XContentBuilder contentBuilder = JsonXContent.contentBuilder(); + contentBuilder.generator().copyCurrentStructure(parser); + return BytesReference.bytes(contentBuilder); + } + private final String id; private final String jobId; - - /** - * The delay before starting to query a period of time - */ private final TimeValue queryDelay; - - /** - * The frequency with which queries are executed - */ private final TimeValue frequency; - private final List indices; private final List types; - private final QueryBuilder query; - private final AggregatorFactories.Builder aggregations; + private final BytesReference query; + private final BytesReference aggregations; private final List scriptFields; private final Integer scrollSize; private final ChunkingConfig chunkingConfig; private DatafeedConfig(String id, String jobId, TimeValue queryDelay, TimeValue frequency, List indices, List types, - QueryBuilder query, AggregatorFactories.Builder aggregations, List scriptFields, + BytesReference query, BytesReference aggregations, List scriptFields, Integer scrollSize, ChunkingConfig chunkingConfig) { this.id = id; this.jobId = jobId; @@ -156,11 +152,11 @@ public class DatafeedConfig implements ToXContentObject { return scrollSize; } - public QueryBuilder getQuery() { + public BytesReference getQuery() { return query; } - public AggregatorFactories.Builder getAggregations() { + public BytesReference getAggregations() { return aggregations; } @@ -183,11 +179,17 @@ public class DatafeedConfig implements ToXContentObject { if (frequency != null) { builder.field(FREQUENCY.getPreferredName(), frequency.getStringRep()); } - builder.field(INDICES.getPreferredName(), indices); - builder.field(TYPES.getPreferredName(), types); - builder.field(QUERY.getPreferredName(), query); + if (indices != null) { + builder.field(INDICES.getPreferredName(), indices); + } + if (types != null) { + builder.field(TYPES.getPreferredName(), types); + } + if (query != null) { + builder.field(QUERY.getPreferredName(), asMap(query)); + } if (aggregations != null) { - builder.field(AGGREGATIONS.getPreferredName(), aggregations); + builder.field(AGGREGATIONS.getPreferredName(), asMap(aggregations)); } if (scriptFields != null) { builder.startObject(SCRIPT_FIELDS.getPreferredName()); @@ -196,7 +198,9 @@ public class DatafeedConfig implements ToXContentObject { } builder.endObject(); } - builder.field(SCROLL_SIZE.getPreferredName(), scrollSize); + if (scrollSize != null) { + builder.field(SCROLL_SIZE.getPreferredName(), scrollSize); + } if (chunkingConfig != null) { builder.field(CHUNKING_CONFIG.getPreferredName(), chunkingConfig); } @@ -205,10 +209,18 @@ public class DatafeedConfig implements ToXContentObject { return builder; } + private static Map asMap(BytesReference bytesReference) { + return bytesReference == null ? null : XContentHelper.convertToMap(bytesReference, true, XContentType.JSON).v2(); + } + /** * The lists of indices and types are compared for equality but they are not * sorted first so this test could fail simply because the indices and types * lists are in different orders. + * + * Also note this could be a heavy operation when a query or aggregations + * are set as we need to convert the bytes references into maps to correctly + * compare them. */ @Override public boolean equals(Object other) { @@ -228,31 +240,40 @@ public class DatafeedConfig implements ToXContentObject { && Objects.equals(this.queryDelay, that.queryDelay) && Objects.equals(this.indices, that.indices) && Objects.equals(this.types, that.types) - && Objects.equals(this.query, that.query) + && Objects.equals(asMap(this.query), asMap(that.query)) && Objects.equals(this.scrollSize, that.scrollSize) - && Objects.equals(this.aggregations, that.aggregations) + && Objects.equals(asMap(this.aggregations), asMap(that.aggregations)) && Objects.equals(this.scriptFields, that.scriptFields) && Objects.equals(this.chunkingConfig, that.chunkingConfig); } + /** + * Note this could be a heavy operation when a query or aggregations + * are set as we need to convert the bytes references into maps to + * compute a stable hash code. + */ @Override public int hashCode() { - return Objects.hash(id, jobId, frequency, queryDelay, indices, types, query, scrollSize, aggregations, scriptFields, + return Objects.hash(id, jobId, frequency, queryDelay, indices, types, asMap(query), scrollSize, asMap(aggregations), scriptFields, chunkingConfig); } + public static Builder builder(String id, String jobId) { + return new Builder(id, jobId); + } + public static class Builder { private String id; private String jobId; private TimeValue queryDelay; private TimeValue frequency; - private List indices = Collections.emptyList(); - private List types = Collections.emptyList(); - private QueryBuilder query = QueryBuilders.matchAllQuery(); - private AggregatorFactories.Builder aggregations; + private List indices; + private List types; + private BytesReference query; + private BytesReference aggregations; private List scriptFields; - private Integer scrollSize = DEFAULT_SCROLL_SIZE; + private Integer scrollSize; private ChunkingConfig chunkingConfig; public Builder(String id, String jobId) { @@ -279,8 +300,12 @@ public class DatafeedConfig implements ToXContentObject { return this; } + public Builder setIndices(String... indices) { + return setIndices(Arrays.asList(indices)); + } + public Builder setTypes(List types) { - this.types = Objects.requireNonNull(types, TYPES.getPreferredName()); + this.types = types; return this; } @@ -294,16 +319,36 @@ public class DatafeedConfig implements ToXContentObject { return this; } - public Builder setQuery(QueryBuilder query) { - this.query = Objects.requireNonNull(query, QUERY.getPreferredName()); + private Builder setQuery(BytesReference query) { + this.query = query; return this; } - public Builder setAggregations(AggregatorFactories.Builder aggregations) { + public Builder setQuery(String queryAsJson) { + this.query = queryAsJson == null ? null : new BytesArray(queryAsJson); + return this; + } + + public Builder setQuery(QueryBuilder query) throws IOException { + this.query = query == null ? null : xContentToBytes(query); + return this; + } + + private Builder setAggregations(BytesReference aggregations) { this.aggregations = aggregations; return this; } + public Builder setAggregations(String aggsAsJson) { + this.aggregations = aggsAsJson == null ? null : new BytesArray(aggsAsJson); + return this; + } + + public Builder setAggregations(AggregatorFactories.Builder aggregations) throws IOException { + this.aggregations = aggregations == null ? null : xContentToBytes(aggregations); + return this; + } + public Builder setScriptFields(List scriptFields) { List sorted = new ArrayList<>(scriptFields); sorted.sort(Comparator.comparing(SearchSourceBuilder.ScriptField::fieldName)); @@ -325,5 +370,12 @@ public class DatafeedConfig implements ToXContentObject { return new DatafeedConfig(id, jobId, queryDelay, frequency, indices, types, query, aggregations, scriptFields, scrollSize, chunkingConfig); } + + private static BytesReference xContentToBytes(ToXContentObject object) throws IOException { + try (XContentBuilder builder = JsonXContent.contentBuilder()) { + object.toXContent(builder, ToXContentObject.EMPTY_PARAMS); + return BytesReference.bytes(builder); + } + } } } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/datafeed/DatafeedUpdate.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/datafeed/DatafeedUpdate.java index 184d5d51481..1e59ea067ca 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/datafeed/DatafeedUpdate.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/datafeed/DatafeedUpdate.java @@ -20,12 +20,17 @@ package org.elasticsearch.client.ml.datafeed; import org.elasticsearch.client.ml.job.config.Job; import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.index.query.AbstractQueryBuilder; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.search.aggregations.AggregatorFactories; import org.elasticsearch.search.builder.SearchSourceBuilder; @@ -35,6 +40,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Map; import java.util.Objects; /** @@ -58,11 +64,9 @@ public class DatafeedUpdate implements ToXContentObject { TimeValue.parseTimeValue(val, DatafeedConfig.QUERY_DELAY.getPreferredName())), DatafeedConfig.QUERY_DELAY); PARSER.declareString((builder, val) -> builder.setFrequency( TimeValue.parseTimeValue(val, DatafeedConfig.FREQUENCY.getPreferredName())), DatafeedConfig.FREQUENCY); - PARSER.declareObject(Builder::setQuery, (p, c) -> AbstractQueryBuilder.parseInnerQueryBuilder(p), DatafeedConfig.QUERY); - PARSER.declareObject(Builder::setAggregations, (p, c) -> AggregatorFactories.parseAggregators(p), - DatafeedConfig.AGGREGATIONS); - PARSER.declareObject(Builder::setAggregations, (p, c) -> AggregatorFactories.parseAggregators(p), - DatafeedConfig.AGGS); + PARSER.declareField(Builder::setQuery, DatafeedUpdate::parseBytes, DatafeedConfig.QUERY, ObjectParser.ValueType.OBJECT); + PARSER.declareField(Builder::setAggregations, DatafeedUpdate::parseBytes, DatafeedConfig.AGGREGATIONS, + ObjectParser.ValueType.OBJECT); PARSER.declareObject(Builder::setScriptFields, (p, c) -> { List parsedScriptFields = new ArrayList<>(); while (p.nextToken() != XContentParser.Token.END_OBJECT) { @@ -74,20 +78,26 @@ public class DatafeedUpdate implements ToXContentObject { PARSER.declareObject(Builder::setChunkingConfig, ChunkingConfig.PARSER, DatafeedConfig.CHUNKING_CONFIG); } + private static BytesReference parseBytes(XContentParser parser) throws IOException { + XContentBuilder contentBuilder = JsonXContent.contentBuilder(); + contentBuilder.generator().copyCurrentStructure(parser); + return BytesReference.bytes(contentBuilder); + } + private final String id; private final String jobId; private final TimeValue queryDelay; private final TimeValue frequency; private final List indices; private final List types; - private final QueryBuilder query; - private final AggregatorFactories.Builder aggregations; + private final BytesReference query; + private final BytesReference aggregations; private final List scriptFields; private final Integer scrollSize; private final ChunkingConfig chunkingConfig; private DatafeedUpdate(String id, String jobId, TimeValue queryDelay, TimeValue frequency, List indices, List types, - QueryBuilder query, AggregatorFactories.Builder aggregations, List scriptFields, + BytesReference query, BytesReference aggregations, List scriptFields, Integer scrollSize, ChunkingConfig chunkingConfig) { this.id = id; this.jobId = jobId; @@ -121,9 +131,13 @@ public class DatafeedUpdate implements ToXContentObject { builder.field(DatafeedConfig.FREQUENCY.getPreferredName(), frequency.getStringRep()); } addOptionalField(builder, DatafeedConfig.INDICES, indices); + if (query != null) { + builder.field(DatafeedConfig.QUERY.getPreferredName(), asMap(query)); + } + if (aggregations != null) { + builder.field(DatafeedConfig.AGGREGATIONS.getPreferredName(), asMap(aggregations)); + } addOptionalField(builder, DatafeedConfig.TYPES, types); - addOptionalField(builder, DatafeedConfig.QUERY, query); - addOptionalField(builder, DatafeedConfig.AGGREGATIONS, aggregations); if (scriptFields != null) { builder.startObject(DatafeedConfig.SCRIPT_FIELDS.getPreferredName()); for (SearchSourceBuilder.ScriptField scriptField : scriptFields) { @@ -167,11 +181,11 @@ public class DatafeedUpdate implements ToXContentObject { return scrollSize; } - public QueryBuilder getQuery() { + public BytesReference getQuery() { return query; } - public AggregatorFactories.Builder getAggregations() { + public BytesReference getAggregations() { return aggregations; } @@ -183,10 +197,18 @@ public class DatafeedUpdate implements ToXContentObject { return chunkingConfig; } + private static Map asMap(BytesReference bytesReference) { + return bytesReference == null ? null : XContentHelper.convertToMap(bytesReference, true, XContentType.JSON).v2(); + } + /** * The lists of indices and types are compared for equality but they are not * sorted first so this test could fail simply because the indices and types * lists are in different orders. + * + * Also note this could be a heavy operation when a query or aggregations + * are set as we need to convert the bytes references into maps to correctly + * compare them. */ @Override public boolean equals(Object other) { @@ -206,19 +228,28 @@ public class DatafeedUpdate implements ToXContentObject { && Objects.equals(this.queryDelay, that.queryDelay) && Objects.equals(this.indices, that.indices) && Objects.equals(this.types, that.types) - && Objects.equals(this.query, that.query) + && Objects.equals(asMap(this.query), asMap(that.query)) && Objects.equals(this.scrollSize, that.scrollSize) - && Objects.equals(this.aggregations, that.aggregations) + && Objects.equals(asMap(this.aggregations), asMap(that.aggregations)) && Objects.equals(this.scriptFields, that.scriptFields) && Objects.equals(this.chunkingConfig, that.chunkingConfig); } + /** + * Note this could be a heavy operation when a query or aggregations + * are set as we need to convert the bytes references into maps to + * compute a stable hash code. + */ @Override public int hashCode() { - return Objects.hash(id, jobId, frequency, queryDelay, indices, types, query, scrollSize, aggregations, scriptFields, + return Objects.hash(id, jobId, frequency, queryDelay, indices, types, asMap(query), scrollSize, asMap(aggregations), scriptFields, chunkingConfig); } + public static Builder builder(String id) { + return new Builder(id); + } + public static class Builder { private String id; @@ -227,8 +258,8 @@ public class DatafeedUpdate implements ToXContentObject { private TimeValue frequency; private List indices; private List types; - private QueryBuilder query; - private AggregatorFactories.Builder aggregations; + private BytesReference query; + private BytesReference aggregations; private List scriptFields; private Integer scrollSize; private ChunkingConfig chunkingConfig; @@ -276,16 +307,36 @@ public class DatafeedUpdate implements ToXContentObject { return this; } - public Builder setQuery(QueryBuilder query) { + private Builder setQuery(BytesReference query) { this.query = query; return this; } - public Builder setAggregations(AggregatorFactories.Builder aggregations) { + public Builder setQuery(String queryAsJson) { + this.query = queryAsJson == null ? null : new BytesArray(queryAsJson); + return this; + } + + public Builder setQuery(QueryBuilder query) throws IOException { + this.query = query == null ? null : xContentToBytes(query); + return this; + } + + private Builder setAggregations(BytesReference aggregations) { this.aggregations = aggregations; return this; } + public Builder setAggregations(String aggsAsJson) { + this.aggregations = aggsAsJson == null ? null : new BytesArray(aggsAsJson); + return this; + } + + public Builder setAggregations(AggregatorFactories.Builder aggregations) throws IOException { + this.aggregations = aggregations == null ? null : xContentToBytes(aggregations); + return this; + } + public Builder setScriptFields(List scriptFields) { List sorted = new ArrayList<>(scriptFields); sorted.sort(Comparator.comparing(SearchSourceBuilder.ScriptField::fieldName)); @@ -307,5 +358,12 @@ public class DatafeedUpdate implements ToXContentObject { return new DatafeedUpdate(id, jobId, queryDelay, frequency, indices, types, query, aggregations, scriptFields, scrollSize, chunkingConfig); } + + private static BytesReference xContentToBytes(ToXContentObject object) throws IOException { + try (XContentBuilder builder = JsonXContent.contentBuilder()) { + object.toXContent(builder, ToXContentObject.EMPTY_PARAMS); + return BytesReference.bytes(builder); + } + } } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java index d63573b534c..19db672e35b 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java @@ -37,8 +37,11 @@ import org.elasticsearch.client.ml.GetOverallBucketsRequest; import org.elasticsearch.client.ml.GetRecordsRequest; import org.elasticsearch.client.ml.OpenJobRequest; import org.elasticsearch.client.ml.PostDataRequest; +import org.elasticsearch.client.ml.PutDatafeedRequest; import org.elasticsearch.client.ml.PutJobRequest; import org.elasticsearch.client.ml.UpdateJobRequest; +import org.elasticsearch.client.ml.datafeed.DatafeedConfig; +import org.elasticsearch.client.ml.datafeed.DatafeedConfigTests; import org.elasticsearch.client.ml.job.config.AnalysisConfig; import org.elasticsearch.client.ml.job.config.Detector; import org.elasticsearch.client.ml.job.config.Job; @@ -206,6 +209,20 @@ public class MLRequestConvertersTests extends ESTestCase { } } + public void testPutDatafeed() throws IOException { + DatafeedConfig datafeed = DatafeedConfigTests.createRandom(); + PutDatafeedRequest putDatafeedRequest = new PutDatafeedRequest(datafeed); + + Request request = MLRequestConverters.putDatafeed(putDatafeedRequest); + + assertEquals(HttpPut.METHOD_NAME, request.getMethod()); + assertThat(request.getEndpoint(), equalTo("/_xpack/ml/datafeeds/" + datafeed.getId())); + try (XContentParser parser = createParser(JsonXContent.jsonXContent, request.getEntity().getContent())) { + DatafeedConfig parsedDatafeed = DatafeedConfig.PARSER.apply(parser, null).build(); + assertThat(parsedDatafeed, equalTo(datafeed)); + } + } + public void testDeleteForecast() throws Exception { String jobId = randomAlphaOfLength(10); DeleteForecastRequest deleteForecastRequest = new DeleteForecastRequest(jobId); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java index db680aaa95d..c0bf1055058 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java @@ -23,34 +23,37 @@ import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.action.get.GetRequest; import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.support.master.AcknowledgedResponse; -import org.elasticsearch.client.ml.DeleteForecastRequest; -import org.elasticsearch.client.ml.ForecastJobRequest; -import org.elasticsearch.client.ml.ForecastJobResponse; -import org.elasticsearch.client.ml.PostDataRequest; -import org.elasticsearch.client.ml.PostDataResponse; -import org.elasticsearch.client.ml.UpdateJobRequest; -import org.elasticsearch.client.ml.job.config.JobUpdate; -import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.client.ml.GetJobStatsRequest; -import org.elasticsearch.client.ml.GetJobStatsResponse; -import org.elasticsearch.client.ml.job.config.JobState; -import org.elasticsearch.client.ml.job.stats.JobStats; import org.elasticsearch.client.ml.CloseJobRequest; import org.elasticsearch.client.ml.CloseJobResponse; +import org.elasticsearch.client.ml.DeleteForecastRequest; import org.elasticsearch.client.ml.DeleteJobRequest; import org.elasticsearch.client.ml.DeleteJobResponse; +import org.elasticsearch.client.ml.FlushJobRequest; +import org.elasticsearch.client.ml.FlushJobResponse; +import org.elasticsearch.client.ml.ForecastJobRequest; +import org.elasticsearch.client.ml.ForecastJobResponse; import org.elasticsearch.client.ml.GetJobRequest; import org.elasticsearch.client.ml.GetJobResponse; +import org.elasticsearch.client.ml.GetJobStatsRequest; +import org.elasticsearch.client.ml.GetJobStatsResponse; import org.elasticsearch.client.ml.OpenJobRequest; import org.elasticsearch.client.ml.OpenJobResponse; +import org.elasticsearch.client.ml.PostDataRequest; +import org.elasticsearch.client.ml.PostDataResponse; +import org.elasticsearch.client.ml.PutDatafeedRequest; +import org.elasticsearch.client.ml.PutDatafeedResponse; import org.elasticsearch.client.ml.PutJobRequest; import org.elasticsearch.client.ml.PutJobResponse; +import org.elasticsearch.client.ml.UpdateJobRequest; +import org.elasticsearch.client.ml.datafeed.DatafeedConfig; import org.elasticsearch.client.ml.job.config.AnalysisConfig; import org.elasticsearch.client.ml.job.config.DataDescription; import org.elasticsearch.client.ml.job.config.Detector; import org.elasticsearch.client.ml.job.config.Job; -import org.elasticsearch.client.ml.FlushJobRequest; -import org.elasticsearch.client.ml.FlushJobResponse; +import org.elasticsearch.client.ml.job.config.JobState; +import org.elasticsearch.client.ml.job.config.JobUpdate; +import org.elasticsearch.client.ml.job.stats.JobStats; +import org.elasticsearch.common.unit.TimeValue; import org.junit.After; import java.io.IOException; @@ -292,6 +295,23 @@ public class MachineLearningIT extends ESRestHighLevelClientTestCase { assertEquals("Updated description", getResponse.jobs().get(0).getDescription()); } + public void testPutDatafeed() throws Exception { + String jobId = randomValidJobId(); + Job job = buildJob(jobId); + MachineLearningClient machineLearningClient = highLevelClient().machineLearning(); + execute(new PutJobRequest(job), machineLearningClient::putJob, machineLearningClient::putJobAsync); + + String datafeedId = "datafeed-" + jobId; + DatafeedConfig datafeedConfig = DatafeedConfig.builder(datafeedId, jobId).setIndices("some_data_index").build(); + + PutDatafeedResponse response = execute(new PutDatafeedRequest(datafeedConfig), machineLearningClient::putDatafeed, + machineLearningClient::putDatafeedAsync); + + DatafeedConfig createdDatafeed = response.getResponse(); + assertThat(createdDatafeed.getId(), equalTo(datafeedId)); + assertThat(createdDatafeed.getIndices(), equalTo(datafeedConfig.getIndices())); + } + public void testDeleteForecast() throws Exception { String jobId = "test-delete-forecast"; diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java index 2da0da0c53f..3e43792ac6a 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java @@ -59,20 +59,24 @@ import org.elasticsearch.client.ml.OpenJobRequest; import org.elasticsearch.client.ml.OpenJobResponse; import org.elasticsearch.client.ml.PostDataRequest; import org.elasticsearch.client.ml.PostDataResponse; +import org.elasticsearch.client.ml.PutDatafeedRequest; +import org.elasticsearch.client.ml.PutDatafeedResponse; import org.elasticsearch.client.ml.PutJobRequest; import org.elasticsearch.client.ml.PutJobResponse; import org.elasticsearch.client.ml.UpdateJobRequest; +import org.elasticsearch.client.ml.datafeed.ChunkingConfig; +import org.elasticsearch.client.ml.datafeed.DatafeedConfig; import org.elasticsearch.client.ml.job.config.AnalysisConfig; import org.elasticsearch.client.ml.job.config.AnalysisLimits; import org.elasticsearch.client.ml.job.config.DataDescription; import org.elasticsearch.client.ml.job.config.DetectionRule; import org.elasticsearch.client.ml.job.config.Detector; import org.elasticsearch.client.ml.job.config.Job; -import org.elasticsearch.client.ml.job.process.DataCounts; import org.elasticsearch.client.ml.job.config.JobUpdate; import org.elasticsearch.client.ml.job.config.ModelPlotConfig; import org.elasticsearch.client.ml.job.config.Operator; import org.elasticsearch.client.ml.job.config.RuleCondition; +import org.elasticsearch.client.ml.job.process.DataCounts; import org.elasticsearch.client.ml.job.results.AnomalyRecord; import org.elasticsearch.client.ml.job.results.Bucket; import org.elasticsearch.client.ml.job.results.CategoryDefinition; @@ -82,6 +86,9 @@ import org.elasticsearch.client.ml.job.stats.JobStats; import org.elasticsearch.client.ml.job.util.PageParams; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.search.aggregations.AggregatorFactories; +import org.elasticsearch.search.builder.SearchSourceBuilder; import org.junit.After; import java.io.IOException; @@ -97,6 +104,7 @@ import java.util.stream.Collectors; import static org.hamcrest.Matchers.closeTo; import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.core.Is.is; @@ -189,8 +197,6 @@ public class MlClientDocumentationIT extends ESRestHighLevelClientTestCase { public void testGetJob() throws Exception { RestHighLevelClient client = highLevelClient(); - String jobId = "get-machine-learning-job1"; - Job job = MachineLearningIT.buildJob("get-machine-learning-job1"); client.machineLearning().putJob(new PutJobRequest(job), RequestOptions.DEFAULT); @@ -481,6 +487,106 @@ public class MlClientDocumentationIT extends ESRestHighLevelClientTestCase { } } + public void testPutDatafeed() throws Exception { + RestHighLevelClient client = highLevelClient(); + + { + // We need to create a job for the datafeed request to be valid + String jobId = "put-datafeed-job-1"; + Job job = MachineLearningIT.buildJob(jobId); + client.machineLearning().putJob(new PutJobRequest(job), RequestOptions.DEFAULT); + + String id = "datafeed-1"; + + //tag::x-pack-ml-create-datafeed-config + DatafeedConfig.Builder datafeedBuilder = new DatafeedConfig.Builder(id, jobId) // <1> + .setIndices("index_1", "index_2"); // <2> + //end::x-pack-ml-create-datafeed-config + + AggregatorFactories.Builder aggs = AggregatorFactories.builder(); + + //tag::x-pack-ml-create-datafeed-config-set-aggregations + datafeedBuilder.setAggregations(aggs); // <1> + //end::x-pack-ml-create-datafeed-config-set-aggregations + + // Clearing aggregation to avoid complex validation rules + datafeedBuilder.setAggregations((String) null); + + //tag::x-pack-ml-create-datafeed-config-set-chunking-config + datafeedBuilder.setChunkingConfig(ChunkingConfig.newAuto()); // <1> + //end::x-pack-ml-create-datafeed-config-set-chunking-config + + //tag::x-pack-ml-create-datafeed-config-set-frequency + datafeedBuilder.setFrequency(TimeValue.timeValueSeconds(30)); // <1> + //end::x-pack-ml-create-datafeed-config-set-frequency + + //tag::x-pack-ml-create-datafeed-config-set-query + datafeedBuilder.setQuery(QueryBuilders.matchAllQuery()); // <1> + //end::x-pack-ml-create-datafeed-config-set-query + + //tag::x-pack-ml-create-datafeed-config-set-query-delay + datafeedBuilder.setQueryDelay(TimeValue.timeValueMinutes(1)); // <1> + //end::x-pack-ml-create-datafeed-config-set-query-delay + + List scriptFields = Collections.emptyList(); + //tag::x-pack-ml-create-datafeed-config-set-script-fields + datafeedBuilder.setScriptFields(scriptFields); // <1> + //end::x-pack-ml-create-datafeed-config-set-script-fields + + //tag::x-pack-ml-create-datafeed-config-set-scroll-size + datafeedBuilder.setScrollSize(1000); // <1> + //end::x-pack-ml-create-datafeed-config-set-scroll-size + + //tag::x-pack-ml-put-datafeed-request + PutDatafeedRequest request = new PutDatafeedRequest(datafeedBuilder.build()); // <1> + //end::x-pack-ml-put-datafeed-request + + //tag::x-pack-ml-put-datafeed-execute + PutDatafeedResponse response = client.machineLearning().putDatafeed(request, RequestOptions.DEFAULT); + //end::x-pack-ml-put-datafeed-execute + + //tag::x-pack-ml-put-datafeed-response + DatafeedConfig datafeed = response.getResponse(); // <1> + //end::x-pack-ml-put-datafeed-response + assertThat(datafeed.getId(), equalTo("datafeed-1")); + } + { + // We need to create a job for the datafeed request to be valid + String jobId = "put-datafeed-job-2"; + Job job = MachineLearningIT.buildJob(jobId); + client.machineLearning().putJob(new PutJobRequest(job), RequestOptions.DEFAULT); + + String id = "datafeed-2"; + + DatafeedConfig datafeed = new DatafeedConfig.Builder(id, jobId).setIndices("index_1", "index_2").build(); + + PutDatafeedRequest request = new PutDatafeedRequest(datafeed); + // tag::x-pack-ml-put-datafeed-execute-listener + ActionListener listener = new ActionListener() { + @Override + public void onResponse(PutDatafeedResponse response) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::x-pack-ml-put-datafeed-execute-listener + + // Replace the empty listener by a blocking listener in test + final CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + + // tag::x-pack-ml-put-datafeed-execute-async + client.machineLearning().putDatafeedAsync(request, RequestOptions.DEFAULT, listener); // <1> + // end::x-pack-ml-put-datafeed-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + } + public void testGetBuckets() throws IOException, InterruptedException { RestHighLevelClient client = highLevelClient(); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PutDatafeedRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PutDatafeedRequestTests.java new file mode 100644 index 00000000000..5af30d32574 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PutDatafeedRequestTests.java @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.client.ml; + +import org.elasticsearch.client.ml.datafeed.DatafeedConfig; +import org.elasticsearch.client.ml.datafeed.DatafeedConfigTests; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + + +public class PutDatafeedRequestTests extends AbstractXContentTestCase { + + @Override + protected PutDatafeedRequest createTestInstance() { + return new PutDatafeedRequest(DatafeedConfigTests.createRandom()); + } + + @Override + protected PutDatafeedRequest doParseInstance(XContentParser parser) { + return new PutDatafeedRequest(DatafeedConfig.PARSER.apply(parser, null).build()); + } + + @Override + protected boolean supportsUnknownFields() { + return false; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PutDatafeedResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PutDatafeedResponseTests.java new file mode 100644 index 00000000000..5b2428167b9 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PutDatafeedResponseTests.java @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.client.ml; + +import org.elasticsearch.client.ml.datafeed.DatafeedConfigTests; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; +import java.util.function.Predicate; + +public class PutDatafeedResponseTests extends AbstractXContentTestCase { + + @Override + protected PutDatafeedResponse createTestInstance() { + return new PutDatafeedResponse(DatafeedConfigTests.createRandom()); + } + + @Override + protected PutDatafeedResponse doParseInstance(XContentParser parser) throws IOException { + return PutDatafeedResponse.fromXContent(parser); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } + + @Override + protected Predicate getRandomFieldsExcludeFilter() { + return field -> !field.isEmpty(); + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/datafeed/DatafeedConfigTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/datafeed/DatafeedConfigTests.java index 8ed51415521..3a7910ad732 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/datafeed/DatafeedConfigTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/datafeed/DatafeedConfigTests.java @@ -19,7 +19,6 @@ package org.elasticsearch.client.ml.datafeed; import com.carrotsearch.randomizedtesting.generators.CodepointSetGenerator; -import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.DeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; @@ -27,7 +26,6 @@ import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.query.QueryBuilders; -import org.elasticsearch.search.SearchModule; import org.elasticsearch.search.aggregations.AggregationBuilders; import org.elasticsearch.search.aggregations.AggregatorFactories; import org.elasticsearch.search.aggregations.metrics.MaxAggregationBuilder; @@ -36,19 +34,26 @@ import org.elasticsearch.test.AbstractXContentTestCase; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; public class DatafeedConfigTests extends AbstractXContentTestCase { @Override protected DatafeedConfig createTestInstance() { + return createRandom(); + } + + public static DatafeedConfig createRandom() { long bucketSpanMillis = 3600000; DatafeedConfig.Builder builder = constructBuilder(); builder.setIndices(randomStringList(1, 10)); builder.setTypes(randomStringList(0, 10)); if (randomBoolean()) { - builder.setQuery(QueryBuilders.termQuery(randomAlphaOfLength(10), randomAlphaOfLength(10))); + try { + builder.setQuery(QueryBuilders.termQuery(randomAlphaOfLength(10), randomAlphaOfLength(10))); + } catch (IOException e) { + throw new RuntimeException("Failed to serialize query", e); + } } boolean addScriptFields = randomBoolean(); if (addScriptFields) { @@ -72,7 +77,11 @@ public class DatafeedConfigTests extends AbstractXContentTestCase randomStringList(int min, int max) { int size = scaledRandomIntBetween(min, max); List list = new ArrayList<>(); @@ -150,21 +153,6 @@ public class DatafeedConfigTests extends AbstractXContentTestCase new DatafeedConfig.Builder(randomValidDatafeedId(), null)); } - public void testCheckValid_GivenNullIndices() { - DatafeedConfig.Builder conf = constructBuilder(); - expectThrows(NullPointerException.class, () -> conf.setIndices(null)); - } - - public void testCheckValid_GivenNullType() { - DatafeedConfig.Builder conf = constructBuilder(); - expectThrows(NullPointerException.class, () -> conf.setTypes(null)); - } - - public void testCheckValid_GivenNullQuery() { - DatafeedConfig.Builder conf = constructBuilder(); - expectThrows(NullPointerException.class, () -> conf.setQuery(null)); - } - public static String randomValidDatafeedId() { CodepointSetGenerator generator = new CodepointSetGenerator("abcdefghijklmnopqrstuvwxyz".toCharArray()); return generator.ofCodePointsLength(random(), 10, 10); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/datafeed/DatafeedUpdateTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/datafeed/DatafeedUpdateTests.java index 3dddad3c016..1c3723fd0a6 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/datafeed/DatafeedUpdateTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/datafeed/DatafeedUpdateTests.java @@ -18,19 +18,16 @@ */ package org.elasticsearch.client.ml.datafeed; -import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.query.QueryBuilders; -import org.elasticsearch.search.SearchModule; import org.elasticsearch.search.aggregations.AggregationBuilders; import org.elasticsearch.search.aggregations.AggregatorFactories; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.test.AbstractXContentTestCase; +import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; public class DatafeedUpdateTests extends AbstractXContentTestCase { @@ -54,7 +51,11 @@ public class DatafeedUpdateTests extends AbstractXContentTestCase The configuration of the {ml} datafeed to create + +[[java-rest-high-x-pack-ml-put-datafeed-config]] +==== Datafeed Configuration + +The `DatafeedConfig` object contains all the details about the {ml} datafeed +configuration. + +A `DatafeedConfig` requires the following arguments: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-create-datafeed-config] +-------------------------------------------------- +<1> The datafeed ID and the job ID +<2> The indices that contain the data to retrieve and feed into the job + +==== Optional Arguments +The following arguments are optional: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-create-datafeed-config-set-chunking-config] +-------------------------------------------------- +<1> Specifies how data searches are split into time chunks. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-create-datafeed-config-set-frequency] +-------------------------------------------------- +<1> The interval at which scheduled queries are made while the datafeed runs in real time. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-create-datafeed-config-set-query] +-------------------------------------------------- +<1> A query to filter the search results by. Defaults to the `match_all` query. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-create-datafeed-config-set-query-delay] +-------------------------------------------------- +<1> The time interval behind real time that data is queried. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-create-datafeed-config-set-script-fields] +-------------------------------------------------- +<1> Allows the use of script fields. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-create-datafeed-config-set-scroll-size] +-------------------------------------------------- +<1> The `size` parameter used in the searches. + +[[java-rest-high-x-pack-ml-put-datafeed-execution]] +==== Execution + +The Put Datafeed API can be executed through a `MachineLearningClient` +instance. Such an instance can be retrieved from a `RestHighLevelClient` +using the `machineLearning()` method: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-put-datafeed-execute] +-------------------------------------------------- + +[[java-rest-high-x-pack-ml-put-datafeed-response]] +==== Response + +The returned `PutDatafeedResponse` returns the full representation of +the new {ml} datafeed if it has been successfully created. This will +contain the creation time and other fields initialized using +default values: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-put-datafeed-response] +-------------------------------------------------- +<1> The created datafeed + +[[java-rest-high-x-pack-ml-put-datafeed-async]] +==== Asynchronous Execution + +This request can be executed asynchronously: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-put-datafeed-execute-async] +-------------------------------------------------- +<1> The `PutDatafeedRequest` to execute and the `ActionListener` to use when +the execution completes + +The asynchronous method does not block and returns immediately. Once it is +completed the `ActionListener` is called back using the `onResponse` method +if the execution successfully completed or using the `onFailure` method if +it failed. + +A typical listener for `PutDatafeedResponse` looks like: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-put-datafeed-execute-listener] +-------------------------------------------------- +<1> Called when the execution is successfully completed. The response is +provided as an argument +<2> Called in case of failure. The raised exception is provided as an argument diff --git a/docs/java-rest/high-level/ml/put-job.asciidoc b/docs/java-rest/high-level/ml/put-job.asciidoc index d51bb63d405..8c726d63b16 100644 --- a/docs/java-rest/high-level/ml/put-job.asciidoc +++ b/docs/java-rest/high-level/ml/put-job.asciidoc @@ -142,7 +142,7 @@ This request can be executed asynchronously: -------------------------------------------------- include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-put-job-execute-async] -------------------------------------------------- -<1> The `PutMlJobRequest` to execute and the `ActionListener` to use when +<1> The `PutJobRequest` to execute and the `ActionListener` to use when the execution completes The asynchronous method does not block and returns immediately. Once it is diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index a6d173f6e27..0be681a14d1 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -220,6 +220,7 @@ The Java High Level REST Client supports the following Machine Learning APIs: * <> * <> * <> +* <> * <> * <> * <> @@ -236,6 +237,7 @@ include::ml/open-job.asciidoc[] include::ml/close-job.asciidoc[] include::ml/update-job.asciidoc[] include::ml/flush-job.asciidoc[] +include::ml/put-datafeed.asciidoc[] include::ml/get-job-stats.asciidoc[] include::ml/forecast-job.asciidoc[] include::ml/delete-forecast.asciidoc[] From bcac7f5e55fcd9a3c3ed0c6b8772dbb57f681a1a Mon Sep 17 00:00:00 2001 From: Tanguy Leroux Date: Wed, 12 Sep 2018 16:03:52 +0200 Subject: [PATCH 31/78] Fix checkstyle violation in ShardFollowNodeTask --- .../org/elasticsearch/xpack/ccr/action/ShardFollowNodeTask.java | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTask.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTask.java index 4237b89e967..c221c097977 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTask.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTask.java @@ -20,7 +20,6 @@ import org.elasticsearch.persistent.AllocatedPersistentTask; import org.elasticsearch.tasks.TaskId; import org.elasticsearch.xpack.ccr.action.bulk.BulkShardOperationsResponse; import org.elasticsearch.xpack.core.ccr.ShardFollowNodeTaskStatus; -import org.elasticsearch.xpack.core.ccr.action.FollowIndexAction; import java.util.ArrayList; import java.util.Arrays; From 36ba3cda7eeb9646f917a0e4338b1eaec9a2b92d Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Wed, 12 Sep 2018 10:53:22 -0400 Subject: [PATCH 32/78] Enable global checkpoint listeners to timeout (#33620) In cross-cluster replication, we will use global checkpoint listeners to long poll for updates to a shard. However, we do not want these polls to wait indefinitely as it could be difficult to discern if the listener is still waiting for updates versus something has gone horribly wrong and cross-cluster replication is stuck. Instead, we want these listeners to timeout after some period (for example, one minute) so that they are notified and we can update status on the following side that cross-cluster replication is still active. After this, we will immediately enter back into a poll mode. To do this, we need the ability to associate a timeout with a global checkpoint listener. This commit adds this capability. --- .../common/util/concurrent/FutureUtils.java | 9 +- .../shard/GlobalCheckpointListeners.java | 113 +++++++-- .../elasticsearch/index/shard/IndexShard.java | 12 +- .../util/concurrent/FutureUtilsTests.java | 41 ++++ .../shard/GlobalCheckpointListenersTests.java | 223 ++++++++++++++---- .../index/shard/IndexShardIT.java | 53 ++++- 6 files changed, 371 insertions(+), 80 deletions(-) create mode 100644 server/src/test/java/org/elasticsearch/common/util/concurrent/FutureUtilsTests.java diff --git a/server/src/main/java/org/elasticsearch/common/util/concurrent/FutureUtils.java b/server/src/main/java/org/elasticsearch/common/util/concurrent/FutureUtils.java index 5e087d3093b..c7345aa3b63 100644 --- a/server/src/main/java/org/elasticsearch/common/util/concurrent/FutureUtils.java +++ b/server/src/main/java/org/elasticsearch/common/util/concurrent/FutureUtils.java @@ -21,6 +21,7 @@ package org.elasticsearch.common.util.concurrent; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchTimeoutException; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.SuppressForbidden; import java.util.concurrent.ExecutionException; @@ -30,8 +31,14 @@ import java.util.concurrent.TimeoutException; public class FutureUtils { + /** + * Cancel execution of this future without interrupting a running thread. See {@link Future#cancel(boolean)} for details. + * + * @param toCancel the future to cancel + * @return false if the future could not be cancelled, otherwise true + */ @SuppressForbidden(reason = "Future#cancel()") - public static boolean cancel(Future toCancel) { + public static boolean cancel(@Nullable final Future toCancel) { if (toCancel != null) { return toCancel.cancel(false); // this method is a forbidden API since it interrupts threads } diff --git a/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java b/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java index 825a8a8a483..df93a935b62 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java +++ b/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java @@ -21,13 +21,19 @@ package org.elasticsearch.index.shard; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.concurrent.FutureUtils; import java.io.Closeable; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.Objects; import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import static org.elasticsearch.index.seqno.SequenceNumbers.NO_OPS_PERFORMED; import static org.elasticsearch.index.seqno.SequenceNumbers.UNASSIGNED_SEQ_NO; @@ -45,38 +51,43 @@ public class GlobalCheckpointListeners implements Closeable { public interface GlobalCheckpointListener { /** * Callback when the global checkpoint is updated or the shard is closed. If the shard is closed, the value of the global checkpoint - * will be set to {@link org.elasticsearch.index.seqno.SequenceNumbers#UNASSIGNED_SEQ_NO} and the exception will be non-null. If the - * global checkpoint is updated, the exception will be null. + * will be set to {@link org.elasticsearch.index.seqno.SequenceNumbers#UNASSIGNED_SEQ_NO} and the exception will be non-null and an + * instance of {@link IndexShardClosedException }. If the listener timed out waiting for notification then the exception will be + * non-null and an instance of {@link TimeoutException}. If the global checkpoint is updated, the exception will be null. * * @param globalCheckpoint the updated global checkpoint - * @param e if non-null, the shard is closed + * @param e if non-null, the shard is closed or the listener timed out */ - void accept(long globalCheckpoint, IndexShardClosedException e); + void accept(long globalCheckpoint, Exception e); } // guarded by this private boolean closed; - private volatile List listeners; + private volatile Map> listeners; private long lastKnownGlobalCheckpoint = UNASSIGNED_SEQ_NO; private final ShardId shardId; private final Executor executor; + private final ScheduledExecutorService scheduler; private final Logger logger; /** * Construct a global checkpoint listeners collection. * - * @param shardId the shard ID on which global checkpoint updates can be listened to - * @param executor the executor for listener notifications - * @param logger a shard-level logger + * @param shardId the shard ID on which global checkpoint updates can be listened to + * @param executor the executor for listener notifications + * @param scheduler the executor used for scheduling timeouts + * @param logger a shard-level logger */ GlobalCheckpointListeners( final ShardId shardId, final Executor executor, + final ScheduledExecutorService scheduler, final Logger logger) { - this.shardId = Objects.requireNonNull(shardId); - this.executor = Objects.requireNonNull(executor); - this.logger = Objects.requireNonNull(logger); + this.shardId = Objects.requireNonNull(shardId, "shardId"); + this.executor = Objects.requireNonNull(executor, "executor"); + this.scheduler = Objects.requireNonNull(scheduler, "scheduler"); + this.logger = Objects.requireNonNull(logger, "logger"); } /** @@ -84,12 +95,15 @@ public class GlobalCheckpointListeners implements Closeable { * listener will be asynchronously notified on the executor used to construct this collection of global checkpoint listeners. If the * shard is closed then the listener will be asynchronously notified on the executor used to construct this collection of global * checkpoint listeners. The listener will only be notified of at most one event, either the global checkpoint is updated or the shard - * is closed. A listener must re-register after one of these events to receive subsequent events. + * is closed. A listener must re-register after one of these events to receive subsequent events. Callers may add a timeout to be + * notified after if the timeout elapses. In this case, the listener will be notified with a {@link TimeoutException}. Passing null for + * the timeout means no timeout will be associated to the listener. * * @param currentGlobalCheckpoint the current global checkpoint known to the listener * @param listener the listener + * @param timeout the listener timeout, or null if no timeout */ - synchronized void add(final long currentGlobalCheckpoint, final GlobalCheckpointListener listener) { + synchronized void add(final long currentGlobalCheckpoint, final GlobalCheckpointListener listener, final TimeValue timeout) { if (closed) { executor.execute(() -> notifyListener(listener, UNASSIGNED_SEQ_NO, new IndexShardClosedException(shardId))); return; @@ -99,9 +113,41 @@ public class GlobalCheckpointListeners implements Closeable { executor.execute(() -> notifyListener(listener, lastKnownGlobalCheckpoint, null)); } else { if (listeners == null) { - listeners = new ArrayList<>(); + listeners = new LinkedHashMap<>(); + } + if (timeout == null) { + listeners.put(listener, null); + } else { + listeners.put( + listener, + scheduler.schedule( + () -> { + final boolean removed; + synchronized (this) { + /* + * Note that the listeners map can be null if a notification nulled out the map reference when + * notifying listeners, and then our scheduled execution occurred before we could be cancelled by + * the notification. In this case, we would have blocked waiting for access to this critical + * section. + * + * What is more, we know that this listener has a timeout associated with it (otherwise we would + * not be here) so the return value from remove being null is an indication that we are not in the + * map. This can happen if a notification nulled out the listeners, and then our scheduled execution + * occurred before we could be cancelled by the notification, and then another thread added a + * listener causing the listeners map reference to be non-null again. In this case, our listener + * here would not be in the map and we should not fire the timeout logic. + */ + removed = listeners != null && listeners.remove(listener) != null; + } + if (removed) { + final TimeoutException e = new TimeoutException(timeout.getStringRep()); + logger.trace("global checkpoint listener timed out", e); + executor.execute(() -> notifyListener(listener, UNASSIGNED_SEQ_NO, e)); + } + }, + timeout.nanos(), + TimeUnit.NANOSECONDS)); } - listeners.add(listener); } } @@ -111,10 +157,25 @@ public class GlobalCheckpointListeners implements Closeable { notifyListeners(UNASSIGNED_SEQ_NO, new IndexShardClosedException(shardId)); } + /** + * The number of listeners currently pending for notification. + * + * @return the number of listeners pending notification + */ synchronized int pendingListeners() { return listeners == null ? 0 : listeners.size(); } + /** + * The scheduled future for a listener that has a timeout associated with it, otherwise null. + * + * @param listener the listener to get the scheduled future for + * @return a scheduled future representing the timeout future for the listener, otherwise null + */ + synchronized ScheduledFuture getTimeoutFuture(final GlobalCheckpointListener listener) { + return listeners.get(listener); + } + /** * Invoke to notify all registered listeners of an updated global checkpoint. * @@ -134,19 +195,24 @@ public class GlobalCheckpointListeners implements Closeable { assert (globalCheckpoint == UNASSIGNED_SEQ_NO && e != null) || (globalCheckpoint >= NO_OPS_PERFORMED && e == null); if (listeners != null) { // capture the current listeners - final List currentListeners = listeners; + final Map> currentListeners = listeners; listeners = null; if (currentListeners != null) { executor.execute(() -> { - for (final GlobalCheckpointListener listener : currentListeners) { - notifyListener(listener, globalCheckpoint, e); + for (final Map.Entry> listener : currentListeners.entrySet()) { + /* + * We do not want to interrupt any timeouts that fired, these will detect that the listener has been notified and + * not trigger the timeout. + */ + FutureUtils.cancel(listener.getValue()); + notifyListener(listener.getKey(), globalCheckpoint, e); } }); } } } - private void notifyListener(final GlobalCheckpointListener listener, final long globalCheckpoint, final IndexShardClosedException e) { + private void notifyListener(final GlobalCheckpointListener listener, final long globalCheckpoint, final Exception e) { try { listener.accept(globalCheckpoint, e); } catch (final Exception caught) { @@ -156,8 +222,11 @@ public class GlobalCheckpointListeners implements Closeable { "error notifying global checkpoint listener of updated global checkpoint [{}]", globalCheckpoint), caught); - } else { + } else if (e instanceof IndexShardClosedException) { logger.warn("error notifying global checkpoint listener of closed shard", caught); + } else { + assert e instanceof TimeoutException : e; + logger.warn("error notifying global checkpoint listener of timeout", caught); } } } diff --git a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java index 4bb56c8b0d3..91d87b00082 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java +++ b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java @@ -302,7 +302,8 @@ public class IndexShard extends AbstractIndexShardComponent implements IndicesCl this.checkIndexOnStartup = indexSettings.getValue(IndexSettings.INDEX_CHECK_ON_STARTUP); this.translogConfig = new TranslogConfig(shardId, shardPath().resolveTranslog(), indexSettings, bigArrays); final String aId = shardRouting.allocationId().getId(); - this.globalCheckpointListeners = new GlobalCheckpointListeners(shardId, threadPool.executor(ThreadPool.Names.LISTENER), logger); + this.globalCheckpointListeners = + new GlobalCheckpointListeners(shardId, threadPool.executor(ThreadPool.Names.LISTENER), threadPool.scheduler(), logger); this.replicationTracker = new ReplicationTracker(shardId, aId, indexSettings, UNASSIGNED_SEQ_NO, globalCheckpointListeners::globalCheckpointUpdated); @@ -1781,15 +1782,18 @@ public class IndexShard extends AbstractIndexShardComponent implements IndicesCl /** * Add a global checkpoint listener. If the global checkpoint is above the current global checkpoint known to the listener then the - * listener will fire immediately on the calling thread. + * listener will fire immediately on the calling thread. If the specified timeout elapses before the listener is notified, the listener + * will be notified with an {@link TimeoutException}. A caller may pass null to specify no timeout. * * @param currentGlobalCheckpoint the current global checkpoint known to the listener * @param listener the listener + * @param timeout the timeout */ public void addGlobalCheckpointListener( final long currentGlobalCheckpoint, - final GlobalCheckpointListeners.GlobalCheckpointListener listener) { - this.globalCheckpointListeners.add(currentGlobalCheckpoint, listener); + final GlobalCheckpointListeners.GlobalCheckpointListener listener, + final TimeValue timeout) { + this.globalCheckpointListeners.add(currentGlobalCheckpoint, listener, timeout); } /** diff --git a/server/src/test/java/org/elasticsearch/common/util/concurrent/FutureUtilsTests.java b/server/src/test/java/org/elasticsearch/common/util/concurrent/FutureUtilsTests.java new file mode 100644 index 00000000000..fb1265dd4d2 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/common/util/concurrent/FutureUtilsTests.java @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.util.concurrent; + +import org.elasticsearch.test.ESTestCase; + +import java.util.concurrent.Future; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class FutureUtilsTests extends ESTestCase { + + public void testCancellingNullFutureOkay() { + FutureUtils.cancel(null); + } + + public void testRunningFutureNotInterrupted() { + final Future future = mock(Future.class); + FutureUtils.cancel(future); + verify(future).cancel(false); + } + +} \ No newline at end of file diff --git a/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java b/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java index bb3a691a702..e5e2453682f 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java @@ -21,8 +21,12 @@ package org.elasticsearch.index.shard; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.index.Index; import org.elasticsearch.test.ESTestCase; +import org.junit.After; import org.mockito.ArgumentCaptor; import java.io.IOException; @@ -35,14 +39,20 @@ import java.util.concurrent.CyclicBarrier; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import static org.elasticsearch.index.seqno.SequenceNumbers.NO_OPS_PERFORMED; import static org.elasticsearch.index.seqno.SequenceNumbers.UNASSIGNED_SEQ_NO; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasToString; +import static org.hamcrest.Matchers.instanceOf; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; @@ -50,10 +60,18 @@ import static org.mockito.Mockito.verify; public class GlobalCheckpointListenersTests extends ESTestCase { - final ShardId shardId = new ShardId(new Index("index", "uuid"), 0); + private final ShardId shardId = new ShardId(new Index("index", "uuid"), 0); + private final ScheduledThreadPoolExecutor scheduler = + new ScheduledThreadPoolExecutor(1, EsExecutors.daemonThreadFactory(Settings.EMPTY, "scheduler")); + + @After + public void shutdownScheduler() { + scheduler.shutdown(); + } public void testGlobalCheckpointUpdated() throws IOException { - final GlobalCheckpointListeners globalCheckpointListeners = new GlobalCheckpointListeners(shardId, Runnable::run, logger); + final GlobalCheckpointListeners globalCheckpointListeners = + new GlobalCheckpointListeners(shardId, Runnable::run, scheduler, logger); globalCheckpointListeners.globalCheckpointUpdated(NO_OPS_PERFORMED); final int numberOfListeners = randomIntBetween(0, 16); final long[] globalCheckpoints = new long[numberOfListeners]; @@ -69,7 +87,7 @@ public class GlobalCheckpointListenersTests extends ESTestCase { assert e == null; globalCheckpoints[index] = g; }; - globalCheckpointListeners.add(NO_OPS_PERFORMED, listener); + globalCheckpointListeners.add(NO_OPS_PERFORMED, listener, null); } final long globalCheckpoint = randomLongBetween(NO_OPS_PERFORMED, Long.MAX_VALUE); globalCheckpointListeners.globalCheckpointUpdated(globalCheckpoint); @@ -92,7 +110,8 @@ public class GlobalCheckpointListenersTests extends ESTestCase { } public void testListenersReadyToBeNotified() throws IOException { - final GlobalCheckpointListeners globalCheckpointListeners = new GlobalCheckpointListeners(shardId, Runnable::run, logger); + final GlobalCheckpointListeners globalCheckpointListeners = + new GlobalCheckpointListeners(shardId, Runnable::run, scheduler, logger); final long globalCheckpoint = randomLongBetween(NO_OPS_PERFORMED + 1, Long.MAX_VALUE); globalCheckpointListeners.globalCheckpointUpdated(globalCheckpoint); final int numberOfListeners = randomIntBetween(0, 16); @@ -109,7 +128,7 @@ public class GlobalCheckpointListenersTests extends ESTestCase { assert e == null; globalCheckpoints[index] = g; }; - globalCheckpointListeners.add(randomLongBetween(NO_OPS_PERFORMED, globalCheckpoint - 1), listener); + globalCheckpointListeners.add(randomLongBetween(NO_OPS_PERFORMED, globalCheckpoint - 1), listener, null); // the listener should be notified immediately assertThat(globalCheckpoints[index], equalTo(globalCheckpoint)); } @@ -130,7 +149,8 @@ public class GlobalCheckpointListenersTests extends ESTestCase { public void testFailingListenerReadyToBeNotified() { final Logger mockLogger = mock(Logger.class); - final GlobalCheckpointListeners globalCheckpointListeners = new GlobalCheckpointListeners(shardId, Runnable::run, mockLogger); + final GlobalCheckpointListeners globalCheckpointListeners = + new GlobalCheckpointListeners(shardId, Runnable::run, scheduler, mockLogger); final long globalCheckpoint = randomLongBetween(NO_OPS_PERFORMED + 1, Long.MAX_VALUE); globalCheckpointListeners.globalCheckpointUpdated(globalCheckpoint); final int numberOfListeners = randomIntBetween(0, 16); @@ -149,7 +169,7 @@ public class GlobalCheckpointListenersTests extends ESTestCase { globalCheckpoints[index] = globalCheckpoint; } }; - globalCheckpointListeners.add(randomLongBetween(NO_OPS_PERFORMED, globalCheckpoint - 1), listener); + globalCheckpointListeners.add(randomLongBetween(NO_OPS_PERFORMED, globalCheckpoint - 1), listener, null); // the listener should be notified immediately if (failure) { assertThat(globalCheckpoints[i], equalTo(Long.MIN_VALUE)); @@ -172,10 +192,11 @@ public class GlobalCheckpointListenersTests extends ESTestCase { } public void testClose() throws IOException { - final GlobalCheckpointListeners globalCheckpointListeners = new GlobalCheckpointListeners(shardId, Runnable::run, logger); + final GlobalCheckpointListeners globalCheckpointListeners = + new GlobalCheckpointListeners(shardId, Runnable::run, scheduler, logger); globalCheckpointListeners.globalCheckpointUpdated(NO_OPS_PERFORMED); final int numberOfListeners = randomIntBetween(0, 16); - final IndexShardClosedException[] exceptions = new IndexShardClosedException[numberOfListeners]; + final Exception[] exceptions = new Exception[numberOfListeners]; for (int i = 0; i < numberOfListeners; i++) { final int index = i; final AtomicBoolean invoked = new AtomicBoolean(); @@ -188,12 +209,13 @@ public class GlobalCheckpointListenersTests extends ESTestCase { assert e != null; exceptions[index] = e; }; - globalCheckpointListeners.add(NO_OPS_PERFORMED, listener); + globalCheckpointListeners.add(NO_OPS_PERFORMED, listener, null); } globalCheckpointListeners.close(); for (int i = 0; i < numberOfListeners; i++) { assertNotNull(exceptions[i]); - assertThat(exceptions[i].getShardId(), equalTo(shardId)); + assertThat(exceptions[i], instanceOf(IndexShardClosedException.class)); + assertThat(((IndexShardClosedException)exceptions[i]).getShardId(), equalTo(shardId)); } // test the listeners are not invoked twice @@ -207,7 +229,8 @@ public class GlobalCheckpointListenersTests extends ESTestCase { } public void testAddAfterClose() throws InterruptedException, IOException { - final GlobalCheckpointListeners globalCheckpointListeners = new GlobalCheckpointListeners(shardId, Runnable::run, logger); + final GlobalCheckpointListeners globalCheckpointListeners = + new GlobalCheckpointListeners(shardId, Runnable::run, scheduler, logger); globalCheckpointListeners.globalCheckpointUpdated(NO_OPS_PERFORMED); globalCheckpointListeners.close(); final AtomicBoolean invoked = new AtomicBoolean(); @@ -221,14 +244,15 @@ public class GlobalCheckpointListenersTests extends ESTestCase { } latch.countDown(); }; - globalCheckpointListeners.add(randomLongBetween(NO_OPS_PERFORMED, Long.MAX_VALUE), listener); + globalCheckpointListeners.add(randomLongBetween(NO_OPS_PERFORMED, Long.MAX_VALUE), listener, null); latch.await(); assertTrue(invoked.get()); } public void testFailingListenerOnUpdate() { final Logger mockLogger = mock(Logger.class); - final GlobalCheckpointListeners globalCheckpointListeners = new GlobalCheckpointListeners(shardId, Runnable::run, mockLogger); + final GlobalCheckpointListeners globalCheckpointListeners = + new GlobalCheckpointListeners(shardId, Runnable::run, scheduler, mockLogger); globalCheckpointListeners.globalCheckpointUpdated(NO_OPS_PERFORMED); final int numberOfListeners = randomIntBetween(0, 16); final boolean[] failures = new boolean[numberOfListeners]; @@ -248,7 +272,7 @@ public class GlobalCheckpointListenersTests extends ESTestCase { globalCheckpoints[index] = g; } }; - globalCheckpointListeners.add(NO_OPS_PERFORMED, listener); + globalCheckpointListeners.add(NO_OPS_PERFORMED, listener, null); } final long globalCheckpoint = randomLongBetween(NO_OPS_PERFORMED, Long.MAX_VALUE); globalCheckpointListeners.globalCheckpointUpdated(globalCheckpoint); @@ -282,11 +306,12 @@ public class GlobalCheckpointListenersTests extends ESTestCase { public void testFailingListenerOnClose() throws IOException { final Logger mockLogger = mock(Logger.class); - final GlobalCheckpointListeners globalCheckpointListeners = new GlobalCheckpointListeners(shardId, Runnable::run, mockLogger); + final GlobalCheckpointListeners globalCheckpointListeners = + new GlobalCheckpointListeners(shardId, Runnable::run, scheduler, mockLogger); globalCheckpointListeners.globalCheckpointUpdated(NO_OPS_PERFORMED); final int numberOfListeners = randomIntBetween(0, 16); final boolean[] failures = new boolean[numberOfListeners]; - final IndexShardClosedException[] exceptions = new IndexShardClosedException[numberOfListeners]; + final Exception[] exceptions = new Exception[numberOfListeners]; for (int i = 0; i < numberOfListeners; i++) { final int index = i; final boolean failure = randomBoolean(); @@ -301,7 +326,7 @@ public class GlobalCheckpointListenersTests extends ESTestCase { exceptions[index] = e; } }; - globalCheckpointListeners.add(NO_OPS_PERFORMED, listener); + globalCheckpointListeners.add(NO_OPS_PERFORMED, listener, null); } globalCheckpointListeners.close(); for (int i = 0; i < numberOfListeners; i++) { @@ -309,7 +334,8 @@ public class GlobalCheckpointListenersTests extends ESTestCase { assertNull(exceptions[i]); } else { assertNotNull(exceptions[i]); - assertThat(exceptions[i].getShardId(), equalTo(shardId)); + assertThat(exceptions[i], instanceOf(IndexShardClosedException.class)); + assertThat(((IndexShardClosedException)exceptions[i]).getShardId(), equalTo(shardId)); } } int failureCount = 0; @@ -334,17 +360,20 @@ public class GlobalCheckpointListenersTests extends ESTestCase { count.incrementAndGet(); command.run(); }; - final GlobalCheckpointListeners globalCheckpointListeners = new GlobalCheckpointListeners(shardId, executor, logger); + final GlobalCheckpointListeners globalCheckpointListeners = new GlobalCheckpointListeners(shardId, executor, scheduler, logger); globalCheckpointListeners.globalCheckpointUpdated(NO_OPS_PERFORMED); final long globalCheckpoint = randomLongBetween(NO_OPS_PERFORMED, Long.MAX_VALUE); final AtomicInteger notified = new AtomicInteger(); final int numberOfListeners = randomIntBetween(0, 16); for (int i = 0; i < numberOfListeners; i++) { - globalCheckpointListeners.add(NO_OPS_PERFORMED, (g, e) -> { - notified.incrementAndGet(); - assertThat(g, equalTo(globalCheckpoint)); - assertNull(e); - }); + globalCheckpointListeners.add( + NO_OPS_PERFORMED, + (g, e) -> { + notified.incrementAndGet(); + assertThat(g, equalTo(globalCheckpoint)); + assertNull(e); + }, + null); } globalCheckpointListeners.globalCheckpointUpdated(globalCheckpoint); assertThat(notified.get(), equalTo(numberOfListeners)); @@ -357,17 +386,21 @@ public class GlobalCheckpointListenersTests extends ESTestCase { count.incrementAndGet(); command.run(); }; - final GlobalCheckpointListeners globalCheckpointListeners = new GlobalCheckpointListeners(shardId, executor, logger); + final GlobalCheckpointListeners globalCheckpointListeners = new GlobalCheckpointListeners(shardId, executor, scheduler, logger); globalCheckpointListeners.close(); final AtomicInteger notified = new AtomicInteger(); final int numberOfListeners = randomIntBetween(0, 16); for (int i = 0; i < numberOfListeners; i++) { - globalCheckpointListeners.add(NO_OPS_PERFORMED, (g, e) -> { - notified.incrementAndGet(); - assertThat(g, equalTo(UNASSIGNED_SEQ_NO)); - assertNotNull(e); - assertThat(e.getShardId(), equalTo(shardId)); - }); + globalCheckpointListeners.add( + NO_OPS_PERFORMED, + (g, e) -> { + notified.incrementAndGet(); + assertThat(g, equalTo(UNASSIGNED_SEQ_NO)); + assertNotNull(e); + assertThat(e, instanceOf(IndexShardClosedException.class)); + assertThat(((IndexShardClosedException) e).getShardId(), equalTo(shardId)); + }, + null); } assertThat(notified.get(), equalTo(numberOfListeners)); assertThat(count.get(), equalTo(numberOfListeners)); @@ -379,17 +412,19 @@ public class GlobalCheckpointListenersTests extends ESTestCase { count.incrementAndGet(); command.run(); }; - final GlobalCheckpointListeners globalCheckpointListeners = new GlobalCheckpointListeners(shardId, executor, logger); + final GlobalCheckpointListeners globalCheckpointListeners = new GlobalCheckpointListeners(shardId, executor, scheduler, logger); final long globalCheckpoint = randomNonNegativeLong(); globalCheckpointListeners.globalCheckpointUpdated(globalCheckpoint); final AtomicInteger notified = new AtomicInteger(); final int numberOfListeners = randomIntBetween(0, 16); for (int i = 0; i < numberOfListeners; i++) { - globalCheckpointListeners.add(randomLongBetween(0, globalCheckpoint), (g, e) -> { - notified.incrementAndGet(); - assertThat(g, equalTo(globalCheckpoint)); - assertNull(e); - }); + globalCheckpointListeners.add( + randomLongBetween(0, globalCheckpoint), + (g, e) -> { + notified.incrementAndGet(); + assertThat(g, equalTo(globalCheckpoint)); + assertNull(e); + }, null); } assertThat(notified.get(), equalTo(numberOfListeners)); assertThat(count.get(), equalTo(numberOfListeners)); @@ -397,18 +432,18 @@ public class GlobalCheckpointListenersTests extends ESTestCase { public void testConcurrency() throws BrokenBarrierException, InterruptedException { final ExecutorService executor = Executors.newFixedThreadPool(randomIntBetween(1, 8)); - final GlobalCheckpointListeners globalCheckpointListeners = new GlobalCheckpointListeners(shardId, executor, logger); + final GlobalCheckpointListeners globalCheckpointListeners = new GlobalCheckpointListeners(shardId, executor, scheduler, logger); final AtomicLong globalCheckpoint = new AtomicLong(NO_OPS_PERFORMED); globalCheckpointListeners.globalCheckpointUpdated(globalCheckpoint.get()); // we are going to synchronize the actions of three threads: the updating thread, the listener thread, and the main test thread final CyclicBarrier barrier = new CyclicBarrier(3); - final int numberOfIterations = randomIntBetween(1, 1024); + final int numberOfIterations = randomIntBetween(1, 4096); final AtomicBoolean closed = new AtomicBoolean(); final Thread updatingThread = new Thread(() -> { // synchronize starting with the listener thread and the main test thread awaitQuietly(barrier); for (int i = 0; i < numberOfIterations; i++) { - if (rarely() && closed.get() == false) { + if (i > numberOfIterations / 2 && rarely() && closed.get() == false) { closed.set(true); try { globalCheckpointListeners.close(); @@ -416,7 +451,7 @@ public class GlobalCheckpointListenersTests extends ESTestCase { throw new UncheckedIOException(e); } } - if (closed.get() == false) { + if (rarely() && closed.get() == false) { globalCheckpointListeners.globalCheckpointUpdated(globalCheckpoint.incrementAndGet()); } } @@ -438,7 +473,8 @@ public class GlobalCheckpointListenersTests extends ESTestCase { if (invocation.compareAndSet(false, true) == false) { throw new IllegalStateException("listener invoked twice"); } - }); + }, + randomBoolean() ? null : TimeValue.timeValueNanos(randomLongBetween(1, TimeUnit.MICROSECONDS.toNanos(1)))); } // synchronize ending with the updating thread and the main test thread awaitQuietly(barrier); @@ -463,6 +499,107 @@ public class GlobalCheckpointListenersTests extends ESTestCase { listenersThread.join(); } + public void testTimeout() throws InterruptedException { + final Logger mockLogger = mock(Logger.class); + final GlobalCheckpointListeners globalCheckpointListeners = + new GlobalCheckpointListeners(shardId, Runnable::run, scheduler, mockLogger); + final TimeValue timeout = TimeValue.timeValueMillis(randomIntBetween(1, 50)); + final AtomicBoolean notified = new AtomicBoolean(); + final CountDownLatch latch = new CountDownLatch(1); + globalCheckpointListeners.add( + NO_OPS_PERFORMED, + (g, e) -> { + try { + notified.set(true); + assertThat(g, equalTo(UNASSIGNED_SEQ_NO)); + assertThat(e, instanceOf(TimeoutException.class)); + assertThat(e, hasToString(containsString(timeout.getStringRep()))); + final ArgumentCaptor message = ArgumentCaptor.forClass(String.class); + final ArgumentCaptor t = ArgumentCaptor.forClass(TimeoutException.class); + verify(mockLogger).trace(message.capture(), t.capture()); + assertThat(message.getValue(), equalTo("global checkpoint listener timed out")); + assertThat(t.getValue(), hasToString(containsString(timeout.getStringRep()))); + } catch (Exception caught) { + fail(e.getMessage()); + } finally { + latch.countDown(); + } + }, + timeout); + latch.await(); + + assertTrue(notified.get()); + } + + public void testTimeoutNotificationUsesExecutor() throws InterruptedException { + final AtomicInteger count = new AtomicInteger(); + final Executor executor = command -> { + count.incrementAndGet(); + command.run(); + }; + final GlobalCheckpointListeners globalCheckpointListeners = new GlobalCheckpointListeners(shardId, executor, scheduler, logger); + final TimeValue timeout = TimeValue.timeValueMillis(randomIntBetween(1, 50)); + final AtomicBoolean notified = new AtomicBoolean(); + final CountDownLatch latch = new CountDownLatch(1); + globalCheckpointListeners.add( + NO_OPS_PERFORMED, + (g, e) -> { + try { + notified.set(true); + assertThat(g, equalTo(UNASSIGNED_SEQ_NO)); + assertThat(e, instanceOf(TimeoutException.class)); + } finally { + latch.countDown(); + } + }, + timeout); + latch.await(); + // ensure the listener notification occurred on the executor + assertTrue(notified.get()); + assertThat(count.get(), equalTo(1)); + } + + public void testFailingListenerAfterTimeout() throws InterruptedException { + final Logger mockLogger = mock(Logger.class); + final GlobalCheckpointListeners globalCheckpointListeners = + new GlobalCheckpointListeners(shardId, Runnable::run, scheduler, mockLogger); + final CountDownLatch latch = new CountDownLatch(1); + final TimeValue timeout = TimeValue.timeValueMillis(randomIntBetween(1, 50)); + globalCheckpointListeners.add( + NO_OPS_PERFORMED, + (g, e) -> { + try { + throw new RuntimeException("failure"); + } finally { + latch.countDown(); + } + }, + timeout); + latch.await(); + final ArgumentCaptor message = ArgumentCaptor.forClass(String.class); + final ArgumentCaptor t = ArgumentCaptor.forClass(RuntimeException.class); + verify(mockLogger).warn(message.capture(), t.capture()); + assertThat(message.getValue(), equalTo("error notifying global checkpoint listener of timeout")); + assertNotNull(t.getValue()); + assertThat(t.getValue(), instanceOf(RuntimeException.class)); + assertThat(t.getValue().getMessage(), equalTo("failure")); + } + + public void testTimeoutCancelledAfterListenerNotified() { + final GlobalCheckpointListeners globalCheckpointListeners = + new GlobalCheckpointListeners(shardId, Runnable::run, scheduler, logger); + final TimeValue timeout = TimeValue.timeValueNanos(Long.MAX_VALUE); + final GlobalCheckpointListeners.GlobalCheckpointListener globalCheckpointListener = (g, e) -> { + assertThat(g, equalTo(NO_OPS_PERFORMED)); + assertNull(e); + }; + globalCheckpointListeners.add(NO_OPS_PERFORMED, globalCheckpointListener, timeout); + final ScheduledFuture future = globalCheckpointListeners.getTimeoutFuture(globalCheckpointListener); + assertNotNull(future); + globalCheckpointListeners.globalCheckpointUpdated(NO_OPS_PERFORMED); + assertTrue(future.isCancelled()); + } + private void awaitQuietly(final CyclicBarrier barrier) { try { barrier.await(); diff --git a/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java b/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java index 87edfcfccb1..8fe1daefe6d 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java +++ b/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java @@ -89,6 +89,7 @@ import java.util.Locale; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -113,6 +114,8 @@ import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.instanceOf; public class IndexShardIT extends ESSingleNodeTestCase { @@ -746,10 +749,11 @@ public class IndexShardIT extends ESSingleNodeTestCase { shard.addGlobalCheckpointListener( i - 1, (g, e) -> { - assert g >= NO_OPS_PERFORMED; - assert e == null; + assertThat(g, greaterThanOrEqualTo(NO_OPS_PERFORMED)); + assertNull(e); globalCheckpoint.set(g); - }); + }, + null); client().prepareIndex("test", "_doc", Integer.toString(i)).setSource("{}", XContentType.JSON).get(); assertBusy(() -> assertThat(globalCheckpoint.get(), equalTo((long) index))); // adding a listener expecting a lower global checkpoint should fire immediately @@ -757,10 +761,11 @@ public class IndexShardIT extends ESSingleNodeTestCase { shard.addGlobalCheckpointListener( randomLongBetween(NO_OPS_PERFORMED, i - 1), (g, e) -> { - assert g >= NO_OPS_PERFORMED; - assert e == null; + assertThat(g, greaterThanOrEqualTo(NO_OPS_PERFORMED)); + assertNull(e); immediateGlobalCheckpint.set(g); - }); + }, + null); assertBusy(() -> assertThat(immediateGlobalCheckpint.get(), equalTo((long) index))); } final AtomicBoolean invoked = new AtomicBoolean(); @@ -768,12 +773,40 @@ public class IndexShardIT extends ESSingleNodeTestCase { numberOfUpdates - 1, (g, e) -> { invoked.set(true); - assert g == UNASSIGNED_SEQ_NO; - assert e != null; - assertThat(e.getShardId(), equalTo(shard.shardId())); - }); + assertThat(g, equalTo(UNASSIGNED_SEQ_NO)); + assertThat(e, instanceOf(IndexShardClosedException.class)); + assertThat(((IndexShardClosedException)e).getShardId(), equalTo(shard.shardId())); + }, + null); shard.close("closed", randomBoolean()); assertBusy(() -> assertTrue(invoked.get())); } + public void testGlobalCheckpointListenerTimeout() throws InterruptedException { + createIndex("test", Settings.builder().put("index.number_of_shards", 1).put("index.number_of_replicas", 0).build()); + ensureGreen(); + final IndicesService indicesService = getInstanceFromNode(IndicesService.class); + final IndexService test = indicesService.indexService(resolveIndex("test")); + final IndexShard shard = test.getShardOrNull(0); + final AtomicBoolean notified = new AtomicBoolean(); + final CountDownLatch latch = new CountDownLatch(1); + final TimeValue timeout = TimeValue.timeValueMillis(randomIntBetween(1, 50)); + shard.addGlobalCheckpointListener( + NO_OPS_PERFORMED, + (g, e) -> { + try { + notified.set(true); + assertThat(g, equalTo(UNASSIGNED_SEQ_NO)); + assertNotNull(e); + assertThat(e, instanceOf(TimeoutException.class)); + assertThat(e.getMessage(), equalTo(timeout.getStringRep())); + } finally { + latch.countDown(); + } + }, + timeout); + latch.await(); + assertTrue(notified.get()); + } + } From fe478c23b797f5bbde8b8db4a177247c5b538424 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BCscher?= Date: Wed, 12 Sep 2018 16:56:03 +0200 Subject: [PATCH 33/78] [Docs] Fix heading in composite-aggregation.asciidoc (#33627) The heading for the "Missing buckets" should be on the same level as the the "Order" section. --- .../aggregations/bucket/composite-aggregation.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/aggregations/bucket/composite-aggregation.asciidoc b/docs/reference/aggregations/bucket/composite-aggregation.asciidoc index 3bfa8d91f8b..0726f5f927e 100644 --- a/docs/reference/aggregations/bucket/composite-aggregation.asciidoc +++ b/docs/reference/aggregations/bucket/composite-aggregation.asciidoc @@ -348,7 +348,7 @@ GET /_search \... will sort the composite bucket in descending order when comparing values from the `date_histogram` source and in ascending order when comparing values from the `terms` source. -====== Missing bucket +==== Missing bucket By default documents without a value for a given source are ignored. It is possible to include them in the response by setting `missing_bucket` to From 141c6ef93ea9df206d25ba874284a7fe58a853d4 Mon Sep 17 00:00:00 2001 From: Vladimir Dolzhenko Date: Wed, 12 Sep 2018 17:18:34 +0200 Subject: [PATCH 34/78] upgrade randomizedrunner to 2.7.0 (#33623) --- buildSrc/version.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/version.properties b/buildSrc/version.properties index 914bae4d2c8..fee9a25aa35 100644 --- a/buildSrc/version.properties +++ b/buildSrc/version.properties @@ -16,7 +16,7 @@ slf4j = 1.6.2 jna = 4.5.1 # test dependencies -randomizedrunner = 2.5.2 +randomizedrunner = 2.7.0 junit = 4.12 httpclient = 4.5.2 # When updating httpcore, please also update server/src/main/resources/org/elasticsearch/bootstrap/test-framework.policy From c783488e971d3099a2fc03446c978fae0286cba8 Mon Sep 17 00:00:00 2001 From: Simon Willnauer Date: Wed, 12 Sep 2018 17:47:10 +0200 Subject: [PATCH 35/78] Add `_source`-only snapshot repository (#32844) This change adds a `_source` only snapshot repository that allows to wrap any existing repository as a _backend_ to snapshot only the `_source` part including live docs markers. Snapshots taken with the `source` repository won't include any indices, doc-values or points. The snapshot will be reduced in size and functionality such that it requires full re-indexing after it's successfully restored. The restore process will copy the `_source` data locally starts a special shard and engine to allow `match_all` scrolls and searches. Any other query, or get call will fail with and unsupported operation exception. The restored index is also marked as read-only. This feature aims mainly for disaster recovery use-cases where snapshot size is a concern or where time to restore is less of an issue. **NOTE**: The snapshot produced by this repository is still a valid lucene index. This change doesn't allow for any longer retention policies which is out of scope for this change. --- docs/reference/modules/snapshots.asciidoc | 45 +++ .../core/internal/io/IOUtils.java | 9 + .../elasticsearch/index/engine/Engine.java | 2 +- .../index/engine/EngineFactory.java | 1 + .../elasticsearch/index/seqno/SeqNoStats.java | 1 - .../shard/AbstractIndexShardComponent.java | 2 - .../org/elasticsearch/index/store/Store.java | 21 +- .../elasticsearch/indices/IndicesService.java | 1 - .../repositories/FilterRepository.java | 167 ++++++++ .../repositories/RepositoriesService.java | 2 +- .../repositories/Repository.java | 13 +- .../blobstore/BlobStoreRepository.java | 15 +- .../snapshots/SnapshotShardsService.java | 3 +- .../index/shard/IndexShardTests.java | 3 +- .../index/engine/EngineTestCase.java | 3 +- .../index/shard/IndexShardTestCase.java | 5 +- .../test/InternalTestCluster.java | 5 +- .../SeqIdGeneratingFilterReader.java | 162 ++++++++ .../snapshots/SourceOnlySnapshot.java | 261 +++++++++++++ .../SourceOnlySnapshotRepository.java | 181 +++++++++ .../elasticsearch/xpack/core/XPackPlugin.java | 30 +- .../snapshots/SourceOnlySnapshotIT.java | 291 ++++++++++++++ .../SourceOnlySnapshotShardTests.java | 358 ++++++++++++++++++ .../snapshots/SourceOnlySnapshotTests.java | 245 ++++++++++++ .../rest-api-spec/test/snapshot/10_basic.yml | 84 ++++ 25 files changed, 1885 insertions(+), 25 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/repositories/FilterRepository.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/snapshots/SeqIdGeneratingFilterReader.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/snapshots/SourceOnlySnapshot.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/snapshots/SourceOnlySnapshotRepository.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotIT.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotShardTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotTests.java create mode 100644 x-pack/plugin/src/test/resources/rest-api-spec/test/snapshot/10_basic.yml diff --git a/docs/reference/modules/snapshots.asciidoc b/docs/reference/modules/snapshots.asciidoc index 0562a677a8d..ba6adf1d35f 100644 --- a/docs/reference/modules/snapshots.asciidoc +++ b/docs/reference/modules/snapshots.asciidoc @@ -207,6 +207,51 @@ repositories.url.allowed_urls: ["http://www.example.org/root/*", "https://*.mydo URL repositories with `file:` URLs can only point to locations registered in the `path.repo` setting similar to shared file system repository. +[float] +[role="xpack"] +[testenv="basic"] +===== Source Only Repository + +A source repository enables you to create minimal, source-only snapshots that take up to 50% less space on disk. +Source only snapshots contain stored fields and index metadata. They do not include index or doc values structures +and are not searchable when restored. After restoring a source-only snapshot, you must <> +the data into a new index. + +Source repositories delegate to another snapshot repository for storage. + + +[IMPORTANT] +================================================== + +Source only snapshots are only supported if the `_source` field is enabled and no source-filtering is applied. +When you restore a source only snapshot: + + * The restored index is read-only and can only serve `match_all` search or scroll requests to enable reindexing. + + * Queries other than `match_all` and `_get` requests are not supported. + + * The mapping of the restored index is empty, but the original mapping is available from the types top + level `meta` element. + +================================================== + +When you create a source repository, you must specify the type and name of the delegate repository +where the snapshots will be stored: + +[source,js] +----------------------------------- +PUT _snapshot/my_src_only_repository +{ + "type": "source", + "settings": { + "delegate_type": "fs", + "location": "my_backup_location" + } +} +----------------------------------- +// CONSOLE +// TEST[continued] + [float] ===== Repository plugins diff --git a/libs/core/src/main/java/org/elasticsearch/core/internal/io/IOUtils.java b/libs/core/src/main/java/org/elasticsearch/core/internal/io/IOUtils.java index 67663516167..493d809f9dc 100644 --- a/libs/core/src/main/java/org/elasticsearch/core/internal/io/IOUtils.java +++ b/libs/core/src/main/java/org/elasticsearch/core/internal/io/IOUtils.java @@ -20,6 +20,7 @@ package org.elasticsearch.core.internal.io; import java.io.Closeable; import java.io.IOException; import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; import java.nio.file.Files; @@ -36,6 +37,14 @@ import java.util.Map; */ public final class IOUtils { + /** + * UTF-8 charset string. + *

Where possible, use {@link StandardCharsets#UTF_8} instead, + * as using the String constant may slow things down. + * @see StandardCharsets#UTF_8 + */ + public static final String UTF_8 = StandardCharsets.UTF_8.name(); + private IOUtils() { // Static utils methods } diff --git a/server/src/main/java/org/elasticsearch/index/engine/Engine.java b/server/src/main/java/org/elasticsearch/index/engine/Engine.java index 5ebe13577f4..fc693113fee 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/Engine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/Engine.java @@ -1594,7 +1594,7 @@ public abstract class Engine implements Closeable { private final CheckedRunnable onClose; private final IndexCommit indexCommit; - IndexCommitRef(IndexCommit indexCommit, CheckedRunnable onClose) { + public IndexCommitRef(IndexCommit indexCommit, CheckedRunnable onClose) { this.indexCommit = indexCommit; this.onClose = onClose; } diff --git a/server/src/main/java/org/elasticsearch/index/engine/EngineFactory.java b/server/src/main/java/org/elasticsearch/index/engine/EngineFactory.java index b477e27b6e1..e50bdd86e75 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/EngineFactory.java +++ b/server/src/main/java/org/elasticsearch/index/engine/EngineFactory.java @@ -21,6 +21,7 @@ package org.elasticsearch.index.engine; /** * Simple Engine Factory */ +@FunctionalInterface public interface EngineFactory { Engine newReadWriteEngine(EngineConfig config); diff --git a/server/src/main/java/org/elasticsearch/index/seqno/SeqNoStats.java b/server/src/main/java/org/elasticsearch/index/seqno/SeqNoStats.java index 9c1795d654c..c711fb42936 100644 --- a/server/src/main/java/org/elasticsearch/index/seqno/SeqNoStats.java +++ b/server/src/main/java/org/elasticsearch/index/seqno/SeqNoStats.java @@ -91,5 +91,4 @@ public class SeqNoStats implements ToXContentFragment, Writeable { ", globalCheckpoint=" + globalCheckpoint + '}'; } - } diff --git a/server/src/main/java/org/elasticsearch/index/shard/AbstractIndexShardComponent.java b/server/src/main/java/org/elasticsearch/index/shard/AbstractIndexShardComponent.java index c56b0d740e7..c967e94f7da 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/AbstractIndexShardComponent.java +++ b/server/src/main/java/org/elasticsearch/index/shard/AbstractIndexShardComponent.java @@ -51,6 +51,4 @@ public abstract class AbstractIndexShardComponent implements IndexShardComponent public String nodeName() { return indexSettings.getNodeName(); } - - } diff --git a/server/src/main/java/org/elasticsearch/index/store/Store.java b/server/src/main/java/org/elasticsearch/index/store/Store.java index b892c5c01fe..8e57caad3b4 100644 --- a/server/src/main/java/org/elasticsearch/index/store/Store.java +++ b/server/src/main/java/org/elasticsearch/index/store/Store.java @@ -1439,11 +1439,28 @@ public class Store extends AbstractIndexShardComponent implements Closeable, Ref */ public void bootstrapNewHistory() throws IOException { metadataLock.writeLock().lock(); - try (IndexWriter writer = newIndexWriter(IndexWriterConfig.OpenMode.APPEND, directory, null)) { - final Map userData = getUserData(writer); + try { + Map userData = readLastCommittedSegmentsInfo().getUserData(); final long maxSeqNo = Long.parseLong(userData.get(SequenceNumbers.MAX_SEQ_NO)); + bootstrapNewHistory(maxSeqNo); + } finally { + metadataLock.writeLock().unlock(); + } + } + + /** + * Marks an existing lucene index with a new history uuid and sets the given maxSeqNo as the local checkpoint + * as well as the maximum sequence number. + * This is used to make sure no existing shard will recovery from this index using ops based recovery. + * @see SequenceNumbers#LOCAL_CHECKPOINT_KEY + * @see SequenceNumbers#MAX_SEQ_NO + */ + public void bootstrapNewHistory(long maxSeqNo) throws IOException { + metadataLock.writeLock().lock(); + try (IndexWriter writer = newIndexWriter(IndexWriterConfig.OpenMode.APPEND, directory, null)) { final Map map = new HashMap<>(); map.put(Engine.HISTORY_UUID_KEY, UUIDs.randomBase64UUID()); + map.put(SequenceNumbers.MAX_SEQ_NO, Long.toString(maxSeqNo)); map.put(SequenceNumbers.LOCAL_CHECKPOINT_KEY, Long.toString(maxSeqNo)); updateCommitData(writer, map); } finally { diff --git a/server/src/main/java/org/elasticsearch/indices/IndicesService.java b/server/src/main/java/org/elasticsearch/indices/IndicesService.java index 1c83a880511..e9f674e14a5 100644 --- a/server/src/main/java/org/elasticsearch/indices/IndicesService.java +++ b/server/src/main/java/org/elasticsearch/indices/IndicesService.java @@ -396,7 +396,6 @@ public class IndicesService extends AbstractLifecycleComponent public IndexService indexService(Index index) { return indices.get(index.getUUID()); } - /** * Returns an IndexService for the specified index if exists otherwise a {@link IndexNotFoundException} is thrown. */ diff --git a/server/src/main/java/org/elasticsearch/repositories/FilterRepository.java b/server/src/main/java/org/elasticsearch/repositories/FilterRepository.java new file mode 100644 index 00000000000..4e8e9b6c7f5 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/repositories/FilterRepository.java @@ -0,0 +1,167 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.repositories; + +import org.apache.lucene.index.IndexCommit; +import org.elasticsearch.Version; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.cluster.metadata.RepositoryMetaData; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.component.Lifecycle; +import org.elasticsearch.common.component.LifecycleListener; +import org.elasticsearch.index.shard.IndexShard; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.index.snapshots.IndexShardSnapshotStatus; +import org.elasticsearch.index.store.Store; +import org.elasticsearch.indices.recovery.RecoveryState; +import org.elasticsearch.snapshots.SnapshotId; +import org.elasticsearch.snapshots.SnapshotInfo; +import org.elasticsearch.snapshots.SnapshotShardFailure; + +import java.io.IOException; +import java.util.List; + +public class FilterRepository implements Repository { + + private final Repository in; + + public FilterRepository(Repository in) { + this.in = in; + } + + @Override + public RepositoryMetaData getMetadata() { + return in.getMetadata(); + } + + @Override + public SnapshotInfo getSnapshotInfo(SnapshotId snapshotId) { + return in.getSnapshotInfo(snapshotId); + } + + @Override + public MetaData getSnapshotGlobalMetaData(SnapshotId snapshotId) { + return in.getSnapshotGlobalMetaData(snapshotId); + } + + @Override + public IndexMetaData getSnapshotIndexMetaData(SnapshotId snapshotId, IndexId index) throws IOException { + return in.getSnapshotIndexMetaData(snapshotId, index); + } + + @Override + public RepositoryData getRepositoryData() { + return in.getRepositoryData(); + } + + @Override + public void initializeSnapshot(SnapshotId snapshotId, List indices, MetaData metaData) { + in.initializeSnapshot(snapshotId, indices, metaData); + } + + @Override + public SnapshotInfo finalizeSnapshot(SnapshotId snapshotId, List indices, long startTime, String failure, int totalShards, + List shardFailures, long repositoryStateId, boolean includeGlobalState) { + return in.finalizeSnapshot(snapshotId, indices, startTime, failure, totalShards, shardFailures, repositoryStateId, + includeGlobalState); + } + + @Override + public void deleteSnapshot(SnapshotId snapshotId, long repositoryStateId) { + in.deleteSnapshot(snapshotId, repositoryStateId); + } + + @Override + public long getSnapshotThrottleTimeInNanos() { + return in.getSnapshotThrottleTimeInNanos(); + } + + @Override + public long getRestoreThrottleTimeInNanos() { + return in.getRestoreThrottleTimeInNanos(); + } + + @Override + public String startVerification() { + return in.startVerification(); + } + + @Override + public void endVerification(String verificationToken) { + in.endVerification(verificationToken); + } + + @Override + public void verify(String verificationToken, DiscoveryNode localNode) { + in.verify(verificationToken, localNode); + } + + @Override + public boolean isReadOnly() { + return in.isReadOnly(); + } + + @Override + public void snapshotShard(IndexShard shard, Store store, SnapshotId snapshotId, IndexId indexId, IndexCommit snapshotIndexCommit, + IndexShardSnapshotStatus snapshotStatus) { + in.snapshotShard(shard, store, snapshotId, indexId, snapshotIndexCommit, snapshotStatus); + } + + @Override + public void restoreShard(IndexShard shard, SnapshotId snapshotId, Version version, IndexId indexId, ShardId snapshotShardId, + RecoveryState recoveryState) { + in.restoreShard(shard, snapshotId, version, indexId, snapshotShardId, recoveryState); + } + + @Override + public IndexShardSnapshotStatus getShardSnapshotStatus(SnapshotId snapshotId, Version version, IndexId indexId, ShardId shardId) { + return in.getShardSnapshotStatus(snapshotId, version, indexId, shardId); + } + + @Override + public Lifecycle.State lifecycleState() { + return in.lifecycleState(); + } + + @Override + public void addLifecycleListener(LifecycleListener listener) { + in.addLifecycleListener(listener); + } + + @Override + public void removeLifecycleListener(LifecycleListener listener) { + in.removeLifecycleListener(listener); + } + + @Override + public void start() { + in.start(); + } + + @Override + public void stop() { + in.stop(); + } + + @Override + public void close() { + in.close(); + } +} diff --git a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java index c6cbaa50cdf..aef4381cd8b 100644 --- a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java +++ b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java @@ -398,7 +398,7 @@ public class RepositoriesService extends AbstractComponent implements ClusterSta "repository type [" + repositoryMetaData.type() + "] does not exist"); } try { - Repository repository = factory.create(repositoryMetaData); + Repository repository = factory.create(repositoryMetaData, typesRegistry::get); repository.start(); return repository; } catch (Exception e) { diff --git a/server/src/main/java/org/elasticsearch/repositories/Repository.java b/server/src/main/java/org/elasticsearch/repositories/Repository.java index c0b45259f99..9f16d26ac75 100644 --- a/server/src/main/java/org/elasticsearch/repositories/Repository.java +++ b/server/src/main/java/org/elasticsearch/repositories/Repository.java @@ -28,6 +28,7 @@ import org.elasticsearch.common.component.LifecycleComponent; import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.snapshots.IndexShardSnapshotStatus; +import org.elasticsearch.index.store.Store; import org.elasticsearch.indices.recovery.RecoveryState; import org.elasticsearch.snapshots.SnapshotId; import org.elasticsearch.snapshots.SnapshotInfo; @@ -35,6 +36,7 @@ import org.elasticsearch.snapshots.SnapshotShardFailure; import java.io.IOException; import java.util.List; +import java.util.function.Function; /** * An interface for interacting with a repository in snapshot and restore. @@ -46,7 +48,7 @@ import java.util.List; *

    *
  • Master calls {@link #initializeSnapshot(SnapshotId, List, org.elasticsearch.cluster.metadata.MetaData)} * with list of indices that will be included into the snapshot
  • - *
  • Data nodes call {@link Repository#snapshotShard(IndexShard, SnapshotId, IndexId, IndexCommit, IndexShardSnapshotStatus)} + *
  • Data nodes call {@link Repository#snapshotShard(IndexShard, Store, SnapshotId, IndexId, IndexCommit, IndexShardSnapshotStatus)} * for each shard
  • *
  • When all shard calls return master calls {@link #finalizeSnapshot} with possible list of failures
  • *
@@ -63,6 +65,10 @@ public interface Repository extends LifecycleComponent { * @param metadata metadata for the repository including name and settings */ Repository create(RepositoryMetaData metadata) throws Exception; + + default Repository create(RepositoryMetaData metaData, Function typeLookup) throws Exception { + return create(metaData); + } } /** @@ -188,14 +194,15 @@ public interface Repository extends LifecycleComponent { *

* As snapshot process progresses, implementation of this method should update {@link IndexShardSnapshotStatus} object and check * {@link IndexShardSnapshotStatus#isAborted()} to see if the snapshot process should be aborted. - * * @param shard shard to be snapshotted + * @param store store to be snapshotted * @param snapshotId snapshot id * @param indexId id for the index being snapshotted * @param snapshotIndexCommit commit point * @param snapshotStatus snapshot status */ - void snapshotShard(IndexShard shard, SnapshotId snapshotId, IndexId indexId, IndexCommit snapshotIndexCommit, IndexShardSnapshotStatus snapshotStatus); + void snapshotShard(IndexShard shard, Store store, SnapshotId snapshotId, IndexId indexId, IndexCommit snapshotIndexCommit, + IndexShardSnapshotStatus snapshotStatus); /** * Restores snapshot of the shard. diff --git a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java index 4c36cc5eed8..df80dd473f1 100644 --- a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java @@ -845,8 +845,9 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp } @Override - public void snapshotShard(IndexShard shard, SnapshotId snapshotId, IndexId indexId, IndexCommit snapshotIndexCommit, IndexShardSnapshotStatus snapshotStatus) { - SnapshotContext snapshotContext = new SnapshotContext(shard, snapshotId, indexId, snapshotStatus, System.currentTimeMillis()); + public void snapshotShard(IndexShard shard, Store store, SnapshotId snapshotId, IndexId indexId, IndexCommit snapshotIndexCommit, + IndexShardSnapshotStatus snapshotStatus) { + SnapshotContext snapshotContext = new SnapshotContext(store, snapshotId, indexId, snapshotStatus, System.currentTimeMillis()); try { snapshotContext.snapshot(snapshotIndexCommit); } catch (Exception e) { @@ -854,7 +855,7 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp if (e instanceof IndexShardSnapshotFailedException) { throw (IndexShardSnapshotFailedException) e; } else { - throw new IndexShardSnapshotFailedException(shard.shardId(), e); + throw new IndexShardSnapshotFailedException(store.shardId(), e); } } } @@ -1157,15 +1158,15 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp /** * Constructs new context * - * @param shard shard to be snapshotted + * @param store store to be snapshotted * @param snapshotId snapshot id * @param indexId the id of the index being snapshotted * @param snapshotStatus snapshot status to report progress */ - SnapshotContext(IndexShard shard, SnapshotId snapshotId, IndexId indexId, IndexShardSnapshotStatus snapshotStatus, long startTime) { - super(snapshotId, Version.CURRENT, indexId, shard.shardId()); + SnapshotContext(Store store, SnapshotId snapshotId, IndexId indexId, IndexShardSnapshotStatus snapshotStatus, long startTime) { + super(snapshotId, Version.CURRENT, indexId, store.shardId()); this.snapshotStatus = snapshotStatus; - this.store = shard.store(); + this.store = store; this.startTime = startTime; } diff --git a/server/src/main/java/org/elasticsearch/snapshots/SnapshotShardsService.java b/server/src/main/java/org/elasticsearch/snapshots/SnapshotShardsService.java index 33b4d852987..88612dbcc50 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/SnapshotShardsService.java +++ b/server/src/main/java/org/elasticsearch/snapshots/SnapshotShardsService.java @@ -389,7 +389,8 @@ public class SnapshotShardsService extends AbstractLifecycleComponent implements try { // we flush first to make sure we get the latest writes snapshotted try (Engine.IndexCommitRef snapshotRef = indexShard.acquireLastIndexCommit(true)) { - repository.snapshotShard(indexShard, snapshot.getSnapshotId(), indexId, snapshotRef.getIndexCommit(), snapshotStatus); + repository.snapshotShard(indexShard, indexShard.store(), snapshot.getSnapshotId(), indexId, snapshotRef.getIndexCommit(), + snapshotStatus); if (logger.isDebugEnabled()) { final IndexShardSnapshotStatus.Copy lastSnapshotStatus = snapshotStatus.asCopy(); logger.debug("snapshot ({}) completed to {} with {}", snapshot, repository, lastSnapshotStatus); diff --git a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java index 0c5d9b1613f..9a5df39a970 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java @@ -2969,7 +2969,8 @@ public class IndexShardTests extends IndexShardTestCase { } @Override - public void snapshotShard(IndexShard shard, SnapshotId snapshotId, IndexId indexId, IndexCommit snapshotIndexCommit, IndexShardSnapshotStatus snapshotStatus) { + public void snapshotShard(IndexShard shard, Store store, SnapshotId snapshotId, IndexId indexId, IndexCommit snapshotIndexCommit, + IndexShardSnapshotStatus snapshotStatus) { } @Override diff --git a/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java b/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java index f9377afe6ed..86f7bd903cc 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java @@ -833,7 +833,8 @@ public abstract class EngineTestCase extends ESTestCase { * Asserts the provided engine has a consistent document history between translog and Lucene index. */ public static void assertConsistentHistoryBetweenTranslogAndLuceneIndex(Engine engine, MapperService mapper) throws IOException { - if (mapper.documentMapper() == null || engine.config().getIndexSettings().isSoftDeleteEnabled() == false) { + if (mapper.documentMapper() == null || engine.config().getIndexSettings().isSoftDeleteEnabled() == false + || (engine instanceof InternalEngine) == false) { return; } final long maxSeqNo = ((InternalEngine) engine).getLocalCheckpointTracker().getMaxSeqNo(); diff --git a/test/framework/src/main/java/org/elasticsearch/index/shard/IndexShardTestCase.java b/test/framework/src/main/java/org/elasticsearch/index/shard/IndexShardTestCase.java index a9e715a1129..78ce5bc500c 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/shard/IndexShardTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/index/shard/IndexShardTestCase.java @@ -126,7 +126,7 @@ public abstract class IndexShardTestCase extends ESTestCase { }; protected ThreadPool threadPool; - private long primaryTerm; + protected long primaryTerm; @Override public void setUp() throws Exception { @@ -753,7 +753,8 @@ public abstract class IndexShardTestCase extends ESTestCase { Index index = shard.shardId().getIndex(); IndexId indexId = new IndexId(index.getName(), index.getUUID()); - repository.snapshotShard(shard, snapshot.getSnapshotId(), indexId, indexCommitRef.getIndexCommit(), snapshotStatus); + repository.snapshotShard(shard, shard.store(), snapshot.getSnapshotId(), indexId, indexCommitRef.getIndexCommit(), + snapshotStatus); } final IndexShardSnapshotStatus.Copy lastSnapshotStatus = snapshotStatus.asCopy(); diff --git a/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java b/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java index 3c46acd0fbe..08aafaea399 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java +++ b/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java @@ -75,6 +75,7 @@ import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexService; import org.elasticsearch.index.engine.CommitStats; import org.elasticsearch.index.engine.Engine; +import org.elasticsearch.index.engine.InternalEngine; import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.shard.IndexShardTestCase; import org.elasticsearch.index.shard.ShardId; @@ -1199,7 +1200,9 @@ public final class InternalTestCluster extends TestCluster { for (IndexService indexService : indexServices) { for (IndexShard indexShard : indexService) { try { - IndexShardTestCase.getTranslog(indexShard).getDeletionPolicy().assertNoOpenTranslogRefs(); + if (IndexShardTestCase.getEngine(indexShard) instanceof InternalEngine) { + IndexShardTestCase.getTranslog(indexShard).getDeletionPolicy().assertNoOpenTranslogRefs(); + } } catch (AlreadyClosedException ok) { // all good } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/snapshots/SeqIdGeneratingFilterReader.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/snapshots/SeqIdGeneratingFilterReader.java new file mode 100644 index 00000000000..8dd5d9d98ca --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/snapshots/SeqIdGeneratingFilterReader.java @@ -0,0 +1,162 @@ +/* + * 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.snapshots; + +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.FilterDirectoryReader; +import org.apache.lucene.index.FilterLeafReader; +import org.apache.lucene.index.LeafReader; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.index.NumericDocValues; +import org.apache.lucene.index.PointValues; +import org.apache.lucene.index.Terms; +import org.elasticsearch.index.mapper.SeqNoFieldMapper; +import org.elasticsearch.index.mapper.VersionFieldMapper; + +import java.io.IOException; +import java.util.IdentityHashMap; +import java.util.Map; + +/** + * This filter reader fakes sequence ID, primary term and version + * for a source only index. + */ +final class SeqIdGeneratingFilterReader extends FilterDirectoryReader { + private final long primaryTerm; + + private SeqIdGeneratingFilterReader(DirectoryReader in, SeqIdGeneratingSubReaderWrapper wrapper) throws IOException { + super(in, wrapper); + primaryTerm = wrapper.primaryTerm; + } + + @Override + protected DirectoryReader doWrapDirectoryReader(DirectoryReader in) throws IOException { + return wrap(in, primaryTerm); + } + + static DirectoryReader wrap(DirectoryReader in, long primaryTerm) throws IOException { + Map ctxMap = new IdentityHashMap<>(); + for (LeafReaderContext leave : in.leaves()) { + ctxMap.put(leave.reader(), leave); + } + return new SeqIdGeneratingFilterReader(in, new SeqIdGeneratingSubReaderWrapper(ctxMap, primaryTerm)); + } + + @Override + public CacheHelper getReaderCacheHelper() { + return in.getReaderCacheHelper(); + } + + private abstract static class FakeNumericDocValues extends NumericDocValues { + private final int maxDoc; + int docID = -1; + + FakeNumericDocValues(int maxDoc) { + this.maxDoc = maxDoc; + } + + @Override + public int docID() { + return docID; + } + + @Override + public int nextDoc() { + if (docID+1 < maxDoc) { + docID++; + } else { + docID = NO_MORE_DOCS; + } + return docID; + } + + @Override + public int advance(int target) { + if (target >= maxDoc) { + docID = NO_MORE_DOCS; + } else { + docID = target; + } + return docID; + } + + @Override + public long cost() { + return maxDoc; + } + + @Override + public boolean advanceExact(int target) { + advance(target); + return docID != NO_MORE_DOCS; + } + } + + private static class SeqIdGeneratingSubReaderWrapper extends SubReaderWrapper { + private final Map ctxMap; + private final long primaryTerm; + + SeqIdGeneratingSubReaderWrapper(Map ctxMap, long primaryTerm) { + this.ctxMap = ctxMap; + this.primaryTerm = primaryTerm; + } + + @Override + public LeafReader wrap(LeafReader reader) { + LeafReaderContext leafReaderContext = ctxMap.get(reader); + final int docBase = leafReaderContext.docBase; + return new FilterLeafReader(reader) { + + @Override + public NumericDocValues getNumericDocValues(String field) throws IOException { + if (SeqNoFieldMapper.NAME.equals(field)) { + return new FakeNumericDocValues(maxDoc()) { + @Override + public long longValue() { + return docBase + docID; + } + }; + } else if (SeqNoFieldMapper.PRIMARY_TERM_NAME.equals(field)) { + return new FakeNumericDocValues(maxDoc()) { + @Override + public long longValue() { + return primaryTerm; + } + }; + } else if (VersionFieldMapper.NAME.equals(field)) { + return new FakeNumericDocValues(maxDoc()) { + @Override + public long longValue() { + return 1; + } + }; + } + return super.getNumericDocValues(field); + } + + @Override + public CacheHelper getCoreCacheHelper() { + return reader.getCoreCacheHelper(); + } + + @Override + public CacheHelper getReaderCacheHelper() { + return reader.getReaderCacheHelper(); + } + + @Override + public Terms terms(String field) { + throw new UnsupportedOperationException("_source only indices can't be searched or filtered"); + } + + @Override + public PointValues getPointValues(String field) { + throw new UnsupportedOperationException("_source only indices can't be searched or filtered"); + } + }; + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/snapshots/SourceOnlySnapshot.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/snapshots/SourceOnlySnapshot.java new file mode 100644 index 00000000000..b7d6a51f45a --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/snapshots/SourceOnlySnapshot.java @@ -0,0 +1,261 @@ +/* + * 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.snapshots; + +import org.apache.lucene.codecs.Codec; +import org.apache.lucene.index.CheckIndex; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.DocValuesType; +import org.apache.lucene.index.FieldInfo; +import org.apache.lucene.index.FieldInfos; +import org.apache.lucene.index.IndexCommit; +import org.apache.lucene.index.IndexFileNames; +import org.apache.lucene.index.IndexOptions; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.LeafReader; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.index.SegmentCommitInfo; +import org.apache.lucene.index.SegmentInfo; +import org.apache.lucene.index.SegmentInfos; +import org.apache.lucene.index.SoftDeletesDirectoryReaderWrapper; +import org.apache.lucene.index.StandardDirectoryReader; +import org.apache.lucene.search.DocIdSetIterator; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.ScoreMode; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Weight; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.IOContext; +import org.apache.lucene.store.IndexOutput; +import org.apache.lucene.store.Lock; +import org.apache.lucene.store.TrackingDirectoryWrapper; +import org.apache.lucene.util.Bits; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.FixedBitSet; +import org.elasticsearch.common.lucene.Lucene; +import org.elasticsearch.core.internal.io.IOUtils; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import static org.apache.lucene.codecs.compressing.CompressingStoredFieldsWriter.FIELDS_EXTENSION; +import static org.apache.lucene.codecs.compressing.CompressingStoredFieldsWriter.FIELDS_INDEX_EXTENSION; + +public class SourceOnlySnapshot { + private final Directory targetDirectory; + private final Supplier deleteByQuerySupplier; + + public SourceOnlySnapshot(Directory targetDirectory, Supplier deleteByQuerySupplier) { + this.targetDirectory = targetDirectory; + this.deleteByQuerySupplier = deleteByQuerySupplier; + } + + public SourceOnlySnapshot(Directory targetDirectory) { + this(targetDirectory, null); + } + + public synchronized List syncSnapshot(IndexCommit commit) throws IOException { + long generation; + Map existingSegments = new HashMap<>(); + if (Lucene.indexExists(targetDirectory)) { + SegmentInfos existingsSegmentInfos = Lucene.readSegmentInfos(targetDirectory); + for (SegmentCommitInfo info : existingsSegmentInfos) { + existingSegments.put(new BytesRef(info.info.getId()), info); + } + generation = existingsSegmentInfos.getGeneration(); + } else { + generation = 1; + } + List createdFiles = new ArrayList<>(); + String segmentFileName; + try (Lock writeLock = targetDirectory.obtainLock(IndexWriter.WRITE_LOCK_NAME); + StandardDirectoryReader reader = (StandardDirectoryReader) DirectoryReader.open(commit)) { + SegmentInfos segmentInfos = reader.getSegmentInfos(); + DirectoryReader wrapper = wrapReader(reader); + List newInfos = new ArrayList<>(); + for (LeafReaderContext ctx : wrapper.leaves()) { + SegmentCommitInfo info = segmentInfos.info(ctx.ord); + LeafReader leafReader = ctx.reader(); + LiveDocs liveDocs = getLiveDocs(leafReader); + if (leafReader.numDocs() != 0) { // fully deleted segments don't need to be processed + SegmentCommitInfo newInfo = syncSegment(info, liveDocs, leafReader.getFieldInfos(), existingSegments, createdFiles); + newInfos.add(newInfo); + } + } + segmentInfos.clear(); + segmentInfos.addAll(newInfos); + segmentInfos.setNextWriteGeneration(Math.max(segmentInfos.getGeneration(), generation)+1); + String pendingSegmentFileName = IndexFileNames.fileNameFromGeneration(IndexFileNames.PENDING_SEGMENTS, + "", segmentInfos.getGeneration()); + try (IndexOutput segnOutput = targetDirectory.createOutput(pendingSegmentFileName, IOContext.DEFAULT)) { + segmentInfos.write(targetDirectory, segnOutput); + } + targetDirectory.sync(Collections.singleton(pendingSegmentFileName)); + targetDirectory.sync(createdFiles); + segmentFileName = IndexFileNames.fileNameFromGeneration(IndexFileNames.SEGMENTS, "", segmentInfos.getGeneration()); + targetDirectory.rename(pendingSegmentFileName, segmentFileName); + } + Lucene.pruneUnreferencedFiles(segmentFileName, targetDirectory); + assert assertCheckIndex(); + return Collections.unmodifiableList(createdFiles); + } + + private LiveDocs getLiveDocs(LeafReader reader) throws IOException { + if (deleteByQuerySupplier != null) { + // we have this additional delete by query functionality to filter out documents before we snapshot them + // we can't filter after the fact since we don't have an index anymore. + Query query = deleteByQuerySupplier.get(); + IndexSearcher s = new IndexSearcher(reader); + s.setQueryCache(null); + Query rewrite = s.rewrite(query); + Weight weight = s.createWeight(rewrite, ScoreMode.COMPLETE_NO_SCORES, 1.0f); + Scorer scorer = weight.scorer(reader.getContext()); + if (scorer != null) { + DocIdSetIterator iterator = scorer.iterator(); + if (iterator != null) { + Bits liveDocs = reader.getLiveDocs(); + final FixedBitSet bits; + if (liveDocs != null) { + bits = FixedBitSet.copyOf(liveDocs); + } else { + bits = new FixedBitSet(reader.maxDoc()); + bits.set(0, reader.maxDoc()); + } + int newDeletes = apply(iterator, bits); + if (newDeletes != 0) { + int numDeletes = reader.numDeletedDocs() + newDeletes; + return new LiveDocs(numDeletes, bits); + } + } + } + } + return new LiveDocs(reader.numDeletedDocs(), reader.getLiveDocs()); + } + + private int apply(DocIdSetIterator iterator, FixedBitSet bits) throws IOException { + int docID = -1; + int newDeletes = 0; + while ((docID = iterator.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) { + if (bits.get(docID)) { + bits.clear(docID); + newDeletes++; + } + } + return newDeletes; + } + + + private boolean assertCheckIndex() throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(1024); + try (CheckIndex checkIndex = new CheckIndex(targetDirectory)) { + checkIndex.setFailFast(true); + checkIndex.setInfoStream(new PrintStream(output, false, IOUtils.UTF_8), false); + CheckIndex.Status status = checkIndex.checkIndex(); + if (status == null || status.clean == false) { + throw new RuntimeException("CheckIndex failed: " + output.toString(IOUtils.UTF_8)); + } + return true; + } + } + + DirectoryReader wrapReader(DirectoryReader reader) throws IOException { + String softDeletesField = null; + for (LeafReaderContext ctx : reader.leaves()) { + String field = ctx.reader().getFieldInfos().getSoftDeletesField(); + if (field != null) { + softDeletesField = field; + break; + } + } + return softDeletesField == null ? reader : new SoftDeletesDirectoryReaderWrapper(reader, softDeletesField); + } + + private SegmentCommitInfo syncSegment(SegmentCommitInfo segmentCommitInfo, LiveDocs liveDocs, FieldInfos fieldInfos, + Map existingSegments, List createdFiles) throws IOException { + SegmentInfo si = segmentCommitInfo.info; + Codec codec = si.getCodec(); + final String segmentSuffix = ""; + SegmentCommitInfo newInfo; + final TrackingDirectoryWrapper trackingDir = new TrackingDirectoryWrapper(targetDirectory); + BytesRef segmentId = new BytesRef(si.getId()); + boolean exists = existingSegments.containsKey(segmentId); + if (exists == false) { + SegmentInfo newSegmentInfo = new SegmentInfo(si.dir, si.getVersion(), si.getMinVersion(), si.name, si.maxDoc(), false, + si.getCodec(), si.getDiagnostics(), si.getId(), si.getAttributes(), null); + // we drop the sort on purpose since the field we sorted on doesn't exist in the target index anymore. + newInfo = new SegmentCommitInfo(newSegmentInfo, 0, 0, -1, -1, -1); + List fieldInfoCopy = new ArrayList<>(fieldInfos.size()); + for (FieldInfo fieldInfo : fieldInfos) { + fieldInfoCopy.add(new FieldInfo(fieldInfo.name, fieldInfo.number, + false, false, false, IndexOptions.NONE, DocValuesType.NONE, -1, fieldInfo.attributes(), 0, 0, + fieldInfo.isSoftDeletesField())); + } + FieldInfos newFieldInfos = new FieldInfos(fieldInfoCopy.toArray(new FieldInfo[0])); + codec.fieldInfosFormat().write(trackingDir, newSegmentInfo, segmentSuffix, newFieldInfos, IOContext.DEFAULT); + newInfo.setFieldInfosFiles(trackingDir.getCreatedFiles()); + String idxFile = IndexFileNames.segmentFileName(newSegmentInfo.name, segmentSuffix, FIELDS_INDEX_EXTENSION); + String dataFile = IndexFileNames.segmentFileName(newSegmentInfo.name, segmentSuffix, FIELDS_EXTENSION); + Directory sourceDir = newSegmentInfo.dir; + if (si.getUseCompoundFile()) { + sourceDir = codec.compoundFormat().getCompoundReader(sourceDir, si, IOContext.DEFAULT); + } + trackingDir.copyFrom(sourceDir, idxFile, idxFile, IOContext.DEFAULT); + trackingDir.copyFrom(sourceDir, dataFile, dataFile, IOContext.DEFAULT); + if (sourceDir != newSegmentInfo.dir) { + sourceDir.close(); + } + } else { + newInfo = existingSegments.get(segmentId); + assert newInfo.info.getUseCompoundFile() == false; + } + if (liveDocs.bits != null && liveDocs.numDeletes != 0 && liveDocs.numDeletes != newInfo.getDelCount()) { + if (newInfo.getDelCount() != 0) { + assert assertLiveDocs(liveDocs.bits, liveDocs.numDeletes); + } + codec.liveDocsFormat().writeLiveDocs(liveDocs.bits, trackingDir, newInfo, liveDocs.numDeletes - newInfo.getDelCount(), + IOContext.DEFAULT); + SegmentCommitInfo info = new SegmentCommitInfo(newInfo.info, liveDocs.numDeletes, 0, newInfo.getNextDelGen(), -1, -1); + info.setFieldInfosFiles(newInfo.getFieldInfosFiles()); + info.info.setFiles(trackingDir.getCreatedFiles()); + newInfo = info; + } + if (exists == false) { + newInfo.info.setFiles(trackingDir.getCreatedFiles()); + codec.segmentInfoFormat().write(trackingDir, newInfo.info, IOContext.DEFAULT); + } + createdFiles.addAll(trackingDir.getCreatedFiles()); + return newInfo; + } + + private boolean assertLiveDocs(Bits liveDocs, int deletes) { + int actualDeletes = 0; + for (int i = 0; i < liveDocs.length(); i++ ) { + if (liveDocs.get(i) == false) { + actualDeletes++; + } + } + assert actualDeletes == deletes : " actual: " + actualDeletes + " deletes: " + deletes; + return true; + } + + private static class LiveDocs { + final int numDeletes; + final Bits bits; + + LiveDocs(int numDeletes, Bits bits) { + this.numDeletes = numDeletes; + this.bits = bits; + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/snapshots/SourceOnlySnapshotRepository.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/snapshots/SourceOnlySnapshotRepository.java new file mode 100644 index 00000000000..a75d5f488ee --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/snapshots/SourceOnlySnapshotRepository.java @@ -0,0 +1,181 @@ +/* + * 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.snapshots; + +import com.carrotsearch.hppc.cursors.ObjectObjectCursor; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexCommit; +import org.apache.lucene.index.SegmentInfos; +import org.apache.lucene.search.Query; +import org.apache.lucene.store.FSDirectory; +import org.apache.lucene.store.SimpleFSDirectory; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.cluster.metadata.MappingMetaData; +import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.cluster.metadata.RepositoryMetaData; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.collect.ImmutableOpenMap; +import org.elasticsearch.common.lucene.search.Queries; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.ShardLock; +import org.elasticsearch.index.engine.EngineFactory; +import org.elasticsearch.index.engine.ReadOnlyEngine; +import org.elasticsearch.index.shard.IndexShard; +import org.elasticsearch.index.shard.ShardPath; +import org.elasticsearch.index.snapshots.IndexShardSnapshotStatus; +import org.elasticsearch.index.store.Store; +import org.elasticsearch.index.translog.TranslogStats; +import org.elasticsearch.repositories.FilterRepository; +import org.elasticsearch.repositories.IndexId; +import org.elasticsearch.repositories.Repository; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; +import java.util.Iterator; +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + *

+ * This is a filter snapshot repository that only snapshots the minimal required information + * that is needed to recreate the index. In other words instead of snapshotting the entire shard + * with all it's lucene indexed fields, doc values, points etc. it only snapshots the the stored + * fields including _source and _routing as well as the live docs in oder to distinguish between + * live and deleted docs. + *

+ *

+ * The repository can wrap any other repository delegating the source only snapshot to it to and read + * from it. For instance a file repository of type fs by passing settings.delegate_type=fs + * at repository creation time. + *

+ * Snapshots restored from source only snapshots are minimal indices that are read-only and only allow + * match_all scroll searches in order to reindex the data. + */ +public final class SourceOnlySnapshotRepository extends FilterRepository { + private static final Setting DELEGATE_TYPE = new Setting<>("delegate_type", "", Function.identity(), Setting.Property + .NodeScope); + public static final Setting SOURCE_ONLY = Setting.boolSetting("index.source_only", false, Setting + .Property.IndexScope, Setting.Property.Final, Setting.Property.PrivateIndex); + + private static final String SNAPSHOT_DIR_NAME = "_snapshot"; + + SourceOnlySnapshotRepository(Repository in) { + super(in); + } + + @Override + public void initializeSnapshot(SnapshotId snapshotId, List indices, MetaData metaData) { + // we process the index metadata at snapshot time. This means if somebody tries to restore + // a _source only snapshot with a plain repository it will be just fine since we already set the + // required engine, that the index is read-only and the mapping to a default mapping + try { + MetaData.Builder builder = MetaData.builder(metaData); + for (IndexId indexId : indices) { + IndexMetaData index = metaData.index(indexId.getName()); + IndexMetaData.Builder indexMetadataBuilder = IndexMetaData.builder(index); + // for a minimal restore we basically disable indexing on all fields and only create an index + // that is valid from an operational perspective. ie. it will have all metadata fields like version/ + // seqID etc. and an indexed ID field such that we can potentially perform updates on them or delete documents. + ImmutableOpenMap mappings = index.getMappings(); + Iterator> iterator = mappings.iterator(); + while (iterator.hasNext()) { + ObjectObjectCursor next = iterator.next(); + // we don't need to obey any routing here stuff is read-only anyway and get is disabled + final String mapping = "{ \"" + next.key + "\": { \"enabled\": false, \"_meta\": " + next.value.source().string() + + " } }"; + indexMetadataBuilder.putMapping(next.key, mapping); + } + indexMetadataBuilder.settings(Settings.builder().put(index.getSettings()) + .put(SOURCE_ONLY.getKey(), true) + .put("index.blocks.write", true)); // read-only! + builder.put(indexMetadataBuilder); + } + super.initializeSnapshot(snapshotId, indices, builder.build()); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + @Override + public void snapshotShard(IndexShard shard, Store store, SnapshotId snapshotId, IndexId indexId, IndexCommit snapshotIndexCommit, + IndexShardSnapshotStatus snapshotStatus) { + if (shard.mapperService().documentMapper() != null // if there is no mapping this is null + && shard.mapperService().documentMapper().sourceMapper().isComplete() == false) { + throw new IllegalStateException("Can't snapshot _source only on an index that has incomplete source ie. has _source disabled " + + "or filters the source"); + } + ShardPath shardPath = shard.shardPath(); + Path dataPath = shardPath.getDataPath(); + // TODO should we have a snapshot tmp directory per shard that is maintained by the system? + Path snapPath = dataPath.resolve(SNAPSHOT_DIR_NAME); + try (FSDirectory directory = new SimpleFSDirectory(snapPath)) { + Store tempStore = new Store(store.shardId(), store.indexSettings(), directory, new ShardLock(store.shardId()) { + @Override + protected void closeInternal() { + // do nothing; + } + }, Store.OnClose.EMPTY); + Supplier querySupplier = shard.mapperService().hasNested() ? Queries::newNestedFilter : null; + // SourceOnlySnapshot will take care of soft- and hard-deletes no special casing needed here + SourceOnlySnapshot snapshot = new SourceOnlySnapshot(tempStore.directory(), querySupplier); + snapshot.syncSnapshot(snapshotIndexCommit); + // we will use the lucene doc ID as the seq ID so we set the local checkpoint to maxDoc with a new index UUID + SegmentInfos segmentInfos = store.readLastCommittedSegmentsInfo(); + tempStore.bootstrapNewHistory(segmentInfos.totalMaxDoc()); + store.incRef(); + try (DirectoryReader reader = DirectoryReader.open(tempStore.directory())) { + IndexCommit indexCommit = reader.getIndexCommit(); + super.snapshotShard(shard, tempStore, snapshotId, indexId, indexCommit, snapshotStatus); + } finally { + store.decRef(); + } + } catch (IOException e) { + // why on earth does this super method not declare IOException + throw new UncheckedIOException(e); + } + } + + /** + * Returns an {@link EngineFactory} for the source only snapshots. + */ + public static EngineFactory getEngineFactory() { + return config -> new ReadOnlyEngine(config, null, new TranslogStats(0, 0, 0, 0, 0), true, + reader -> { + try { + return SeqIdGeneratingFilterReader.wrap(reader, config.getPrimaryTermSupplier().getAsLong()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } + + /** + * Returns a new source only repository factory + */ + public static Repository.Factory newRepositoryFactory() { + return new Repository.Factory() { + + @Override + public Repository create(RepositoryMetaData metadata) { + throw new UnsupportedOperationException(); + } + + @Override + public Repository create(RepositoryMetaData metaData, Function typeLookup) throws Exception { + String delegateType = DELEGATE_TYPE.get(metaData.settings()); + if (Strings.hasLength(delegateType) == false) { + throw new IllegalArgumentException(DELEGATE_TYPE.getKey() + " must be set"); + } + Repository.Factory factory = typeLookup.apply(delegateType); + return new SourceOnlySnapshotRepository(factory.create(new RepositoryMetaData(metaData.name(), + delegateType, metaData.settings()), typeLookup)); + } + }; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackPlugin.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackPlugin.java index aaa3effcfe8..ca76e71e052 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackPlugin.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackPlugin.java @@ -31,21 +31,28 @@ import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.logging.ESLoggerFactory; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.IndexScopedSettings; +import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.SettingsFilter; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.env.Environment; import org.elasticsearch.env.NodeEnvironment; +import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.engine.EngineFactory; import org.elasticsearch.license.LicenseService; import org.elasticsearch.license.LicensesMetaData; import org.elasticsearch.license.Licensing; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.persistent.PersistentTaskParams; +import org.elasticsearch.plugins.EnginePlugin; import org.elasticsearch.plugins.ExtensiblePlugin; +import org.elasticsearch.plugins.RepositoryPlugin; import org.elasticsearch.plugins.ScriptPlugin; +import org.elasticsearch.repositories.Repository; import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestHandler; import org.elasticsearch.script.ScriptService; +import org.elasticsearch.snapshots.SourceOnlySnapshotRepository; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.core.action.TransportXPackInfoAction; @@ -67,13 +74,15 @@ import java.security.PrivilegedAction; import java.time.Clock; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.StreamSupport; -public class XPackPlugin extends XPackClientPlugin implements ScriptPlugin, ExtensiblePlugin { +public class XPackPlugin extends XPackClientPlugin implements ScriptPlugin, ExtensiblePlugin, RepositoryPlugin, EnginePlugin { private static Logger logger = ESLoggerFactory.getLogger(XPackPlugin.class); private static DeprecationLogger deprecationLogger = new DeprecationLogger(logger); @@ -340,4 +349,23 @@ public class XPackPlugin extends XPackClientPlugin implements ScriptPlugin, Exte } } + @Override + public Map getRepositories(Environment env, NamedXContentRegistry namedXContentRegistry) { + return Collections.singletonMap("source", SourceOnlySnapshotRepository.newRepositoryFactory()); + } + + @Override + public Optional getEngineFactory(IndexSettings indexSettings) { + if (indexSettings.getValue(SourceOnlySnapshotRepository.SOURCE_ONLY)) { + return Optional.of(SourceOnlySnapshotRepository.getEngineFactory()); + } + return Optional.empty(); + } + + @Override + public List> getSettings() { + List> settings = super.getSettings(); + settings.add(SourceOnlySnapshotRepository.SOURCE_ONLY); + return settings; + } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotIT.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotIT.java new file mode 100644 index 00000000000..6d3a17e3ebf --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotIT.java @@ -0,0 +1,291 @@ +/* + * 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.snapshots; + +import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse; +import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotResponse; +import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder; +import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsResponse; +import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse; +import org.elasticsearch.action.index.IndexRequestBuilder; +import org.elasticsearch.action.search.SearchPhaseExecutionException; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.block.ClusterBlockException; +import org.elasticsearch.cluster.metadata.MappingMetaData; +import org.elasticsearch.common.collect.ImmutableOpenMap; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.env.Environment; +import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.MockEngineFactoryPlugin; +import org.elasticsearch.index.engine.EngineFactory; +import org.elasticsearch.index.mapper.SeqNoFieldMapper; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.node.Node; +import org.elasticsearch.plugins.EnginePlugin; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.plugins.RepositoryPlugin; +import org.elasticsearch.repositories.Repository; +import org.elasticsearch.search.SearchHit; +import org.elasticsearch.search.SearchHits; +import org.elasticsearch.search.slice.SliceBuilder; +import org.elasticsearch.search.sort.SortOrder; +import org.elasticsearch.test.ESIntegTestCase; +import org.hamcrest.Matchers; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount; + +public class SourceOnlySnapshotIT extends ESIntegTestCase { + + @Override + protected Collection> nodePlugins() { + Collection> classes = new ArrayList<>(super.nodePlugins()); + classes.add(MyPlugin.class); + return classes; + } + + @Override + protected Collection> getMockPlugins() { + Collection> classes = new ArrayList<>(super.getMockPlugins()); + classes.remove(MockEngineFactoryPlugin.class); + return classes; + } + + public static final class MyPlugin extends Plugin implements RepositoryPlugin, EnginePlugin { + @Override + public Map getRepositories(Environment env, NamedXContentRegistry namedXContentRegistry) { + return Collections.singletonMap("source", SourceOnlySnapshotRepository.newRepositoryFactory()); + } + @Override + public Optional getEngineFactory(IndexSettings indexSettings) { + if (indexSettings.getValue(SourceOnlySnapshotRepository.SOURCE_ONLY)) { + return Optional.of(SourceOnlySnapshotRepository.getEngineFactory()); + } + return Optional.empty(); + } + + @Override + public List> getSettings() { + List> settings = new ArrayList<>(super.getSettings()); + settings.add(SourceOnlySnapshotRepository.SOURCE_ONLY); + return settings; + } + } + + public void testSnapshotAndRestore() throws Exception { + final String sourceIdx = "test-idx"; + boolean requireRouting = randomBoolean(); + boolean useNested = randomBoolean(); + IndexRequestBuilder[] builders = snashotAndRestore(sourceIdx, 1, true, requireRouting, useNested); + assertHits(sourceIdx, builders.length); + assertMappings(sourceIdx, requireRouting, useNested); + SearchPhaseExecutionException e = expectThrows(SearchPhaseExecutionException.class, () -> { + client().prepareSearch(sourceIdx).setQuery(QueryBuilders.idsQuery() + .addIds("" + randomIntBetween(0, builders.length))).get(); + }); + assertTrue(e.toString().contains("_source only indices can't be searched or filtered")); + + e = expectThrows(SearchPhaseExecutionException.class, () -> + client().prepareSearch(sourceIdx).setQuery(QueryBuilders.termQuery("field1", "bar")).get()); + assertTrue(e.toString().contains("_source only indices can't be searched or filtered")); + // make sure deletes do not work + String idToDelete = "" + randomIntBetween(0, builders.length); + expectThrows(ClusterBlockException.class, () -> client().prepareDelete(sourceIdx, "_doc", idToDelete) + .setRouting("r" + idToDelete).get()); + internalCluster().ensureAtLeastNumDataNodes(2); + client().admin().indices().prepareUpdateSettings(sourceIdx) + .setSettings(Settings.builder().put("index.number_of_replicas", 1)).get(); + ensureGreen(sourceIdx); + assertHits(sourceIdx, builders.length); + } + + public void testSnapshotAndRestoreWithNested() throws Exception { + final String sourceIdx = "test-idx"; + boolean requireRouting = randomBoolean(); + IndexRequestBuilder[] builders = snashotAndRestore(sourceIdx, 1, true, requireRouting, true); + IndicesStatsResponse indicesStatsResponse = client().admin().indices().prepareStats().clear().setDocs(true).get(); + assertThat(indicesStatsResponse.getTotal().docs.getDeleted(), Matchers.greaterThan(0L)); + assertHits(sourceIdx, builders.length); + assertMappings(sourceIdx, requireRouting, true); + SearchPhaseExecutionException e = expectThrows(SearchPhaseExecutionException.class, () -> + client().prepareSearch(sourceIdx).setQuery(QueryBuilders.idsQuery().addIds("" + randomIntBetween(0, builders.length))).get()); + assertTrue(e.toString().contains("_source only indices can't be searched or filtered")); + e = expectThrows(SearchPhaseExecutionException.class, () -> + client().prepareSearch(sourceIdx).setQuery(QueryBuilders.termQuery("field1", "bar")).get()); + assertTrue(e.toString().contains("_source only indices can't be searched or filtered")); + // make sure deletes do not work + String idToDelete = "" + randomIntBetween(0, builders.length); + expectThrows(ClusterBlockException.class, () -> client().prepareDelete(sourceIdx, "_doc", idToDelete) + .setRouting("r" + idToDelete).get()); + internalCluster().ensureAtLeastNumDataNodes(2); + client().admin().indices().prepareUpdateSettings(sourceIdx).setSettings(Settings.builder().put("index.number_of_replicas", 1)) + .get(); + ensureGreen(sourceIdx); + assertHits(sourceIdx, builders.length); + } + + private void assertMappings(String sourceIdx, boolean requireRouting, boolean useNested) throws IOException { + GetMappingsResponse getMappingsResponse = client().admin().indices().prepareGetMappings(sourceIdx).get(); + ImmutableOpenMap mapping = getMappingsResponse + .getMappings().get(sourceIdx); + assertTrue(mapping.containsKey("_doc")); + String nested = useNested ? + ",\"incorrect\":{\"type\":\"object\"},\"nested\":{\"type\":\"nested\",\"properties\":{\"value\":{\"type\":\"long\"}}}" : ""; + if (requireRouting) { + assertEquals("{\"_doc\":{\"enabled\":false," + + "\"_meta\":{\"_doc\":{\"_routing\":{\"required\":true}," + + "\"properties\":{\"field1\":{\"type\":\"text\"," + + "\"fields\":{\"keyword\":{\"type\":\"keyword\",\"ignore_above\":256}}}" + nested + + "}}}}}", mapping.get("_doc").source().string()); + } else { + assertEquals("{\"_doc\":{\"enabled\":false," + + "\"_meta\":{\"_doc\":{\"properties\":{\"field1\":{\"type\":\"text\"," + + "\"fields\":{\"keyword\":{\"type\":\"keyword\",\"ignore_above\":256}}}" + nested + "}}}}}", + mapping.get("_doc").source().string()); + } + } + + private void assertHits(String index, int numDocsExpected) { + SearchResponse searchResponse = client().prepareSearch(index) + .addSort(SeqNoFieldMapper.NAME, SortOrder.ASC) + .setSize(numDocsExpected).get(); + Consumer assertConsumer = res -> { + SearchHits hits = res.getHits(); + IndicesStatsResponse indicesStatsResponse = client().admin().indices().prepareStats().clear().setDocs(true).get(); + long deleted = indicesStatsResponse.getTotal().docs.getDeleted(); + boolean allowHoles = deleted > 0; // we use indexRandom which might create holes ie. deleted docs + long i = 0; + for (SearchHit hit : hits) { + String id = hit.getId(); + Map sourceAsMap = hit.getSourceAsMap(); + assertTrue(sourceAsMap.containsKey("field1")); + if (allowHoles) { + long seqId = ((Number) hit.getSortValues()[0]).longValue(); + assertThat(i, Matchers.lessThanOrEqualTo(seqId)); + i = seqId + 1; + } else { + assertEquals(i++, hit.getSortValues()[0]); + } + assertEquals("bar " + id, sourceAsMap.get("field1")); + assertEquals("r" + id, hit.field("_routing").getValue()); + } + }; + assertConsumer.accept(searchResponse); + assertEquals(numDocsExpected, searchResponse.getHits().totalHits); + searchResponse = client().prepareSearch(index) + .addSort(SeqNoFieldMapper.NAME, SortOrder.ASC) + .setScroll("1m") + .slice(new SliceBuilder(SeqNoFieldMapper.NAME, randomIntBetween(0,1), 2)) + .setSize(randomIntBetween(1, 10)).get(); + do { + // now do a scroll with a slice + assertConsumer.accept(searchResponse); + searchResponse = client().prepareSearchScroll(searchResponse.getScrollId()).setScroll(TimeValue.timeValueMinutes(1)).get(); + } while (searchResponse.getHits().getHits().length > 0); + + } + + private IndexRequestBuilder[] snashotAndRestore(String sourceIdx, int numShards, boolean minimal, boolean requireRouting, boolean + useNested) + throws ExecutionException, InterruptedException, IOException { + logger.info("--> starting a master node and a data node"); + internalCluster().startMasterOnlyNode(); + internalCluster().startDataOnlyNode(); + + final Client client = client(); + final String repo = "test-repo"; + final String snapshot = "test-snap"; + + logger.info("--> creating repository"); + assertAcked(client.admin().cluster().preparePutRepository(repo).setType("source") + .setSettings(Settings.builder().put("location", randomRepoPath()) + .put("delegate_type", "fs") + .put("restore_minimal", minimal) + .put("compress", randomBoolean()))); + + CreateIndexRequestBuilder createIndexRequestBuilder = prepareCreate(sourceIdx, 0, Settings.builder() + .put("number_of_shards", numShards).put("number_of_replicas", 0)); + List mappings = new ArrayList<>(); + if (requireRouting) { + mappings.addAll(Arrays.asList("_routing", "required=true")); + } + + if (useNested) { + mappings.addAll(Arrays.asList("nested", "type=nested", "incorrect", "type=object")); + } + if (mappings.isEmpty() == false) { + createIndexRequestBuilder.addMapping("_doc", mappings.toArray()); + } + assertAcked(createIndexRequestBuilder); + ensureGreen(); + + logger.info("--> indexing some data"); + IndexRequestBuilder[] builders = new IndexRequestBuilder[randomIntBetween(10, 100)]; + for (int i = 0; i < builders.length; i++) { + XContentBuilder source = jsonBuilder() + .startObject() + .field("field1", "bar " + i); + if (useNested) { + source.startArray("nested"); + for (int j = 0; j < 2; ++j) { + source = source.startObject().field("value", i + 1 + j).endObject(); + } + source.endArray(); + } + source.endObject(); + builders[i] = client().prepareIndex(sourceIdx, "_doc", + Integer.toString(i)).setSource(source).setRouting("r" + i); + } + indexRandom(true, builders); + flushAndRefresh(); + assertHitCount(client().prepareSearch(sourceIdx).setQuery(QueryBuilders.idsQuery().addIds("0")).get(), 1); + + logger.info("--> snapshot the index"); + CreateSnapshotResponse createResponse = client.admin().cluster() + .prepareCreateSnapshot(repo, snapshot) + .setWaitForCompletion(true).setIndices(sourceIdx).get(); + assertEquals(SnapshotState.SUCCESS, createResponse.getSnapshotInfo().state()); + + logger.info("--> delete index and stop the data node"); + assertAcked(client.admin().indices().prepareDelete(sourceIdx).get()); + internalCluster().stopRandomDataNode(); + client().admin().cluster().prepareHealth().setTimeout("30s").setWaitForNodes("1"); + + logger.info("--> start a new data node"); + final Settings dataSettings = Settings.builder() + .put(Node.NODE_NAME_SETTING.getKey(), randomAlphaOfLength(5)) + .put(Environment.PATH_HOME_SETTING.getKey(), createTempDir()) // to get a new node id + .build(); + internalCluster().startDataOnlyNode(dataSettings); + client().admin().cluster().prepareHealth().setTimeout("30s").setWaitForNodes("2"); + + logger.info("--> restore the index and ensure all shards are allocated"); + RestoreSnapshotResponse restoreResponse = client().admin().cluster() + .prepareRestoreSnapshot(repo, snapshot).setWaitForCompletion(true) + .setIndices(sourceIdx).get(); + assertEquals(restoreResponse.getRestoreInfo().totalShards(), + restoreResponse.getRestoreInfo().successfulShards()); + ensureYellow(); + return builders; + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotShardTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotShardTests.java new file mode 100644 index 00000000000..7058724ecf0 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotShardTests.java @@ -0,0 +1,358 @@ +/* + * 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.snapshots; + +import org.apache.lucene.document.Document; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexableField; +import org.apache.lucene.index.LeafReader; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.FieldDoc; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.search.Sort; +import org.apache.lucene.search.SortField; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.util.Bits; +import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.Version; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.cluster.metadata.MappingMetaData; +import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.cluster.metadata.RepositoryMetaData; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.cluster.routing.RecoverySource; +import org.elasticsearch.cluster.routing.ShardRouting; +import org.elasticsearch.cluster.routing.ShardRoutingState; +import org.elasticsearch.cluster.routing.TestShardRouting; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.lucene.uid.Versions; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.core.internal.io.IOUtils; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.index.VersionType; +import org.elasticsearch.index.engine.Engine; +import org.elasticsearch.index.engine.EngineException; +import org.elasticsearch.index.engine.InternalEngineFactory; +import org.elasticsearch.index.fieldvisitor.FieldsVisitor; +import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.index.mapper.SeqNoFieldMapper; +import org.elasticsearch.index.mapper.Uid; +import org.elasticsearch.index.seqno.SeqNoStats; +import org.elasticsearch.index.shard.IndexShard; +import org.elasticsearch.index.shard.IndexShardState; +import org.elasticsearch.index.shard.IndexShardTestCase; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.index.snapshots.IndexShardSnapshotStatus; +import org.elasticsearch.indices.recovery.RecoveryState; +import org.elasticsearch.repositories.IndexId; +import org.elasticsearch.repositories.Repository; +import org.elasticsearch.repositories.fs.FsRepository; +import org.elasticsearch.threadpool.ThreadPool; +import org.hamcrest.Matchers; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; + +import static org.elasticsearch.index.mapper.SourceToParse.source; + +public class SourceOnlySnapshotShardTests extends IndexShardTestCase { + + public void testSourceIncomplete() throws IOException { + ShardRouting shardRouting = TestShardRouting.newShardRouting(new ShardId("index", "_na_", 0), randomAlphaOfLength(10), true, + ShardRoutingState.INITIALIZING, RecoverySource.EmptyStoreRecoverySource.INSTANCE); + Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) + .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0) + .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) + .build(); + IndexMetaData metaData = IndexMetaData.builder(shardRouting.getIndexName()) + .settings(settings) + .primaryTerm(0, primaryTerm) + .putMapping("_doc", + "{\"_source\":{\"enabled\": false}}").build(); + IndexShard shard = newShard(shardRouting, metaData, new InternalEngineFactory()); + recoverShardFromStore(shard); + + for (int i = 0; i < 1; i++) { + final String id = Integer.toString(i); + indexDoc(shard, "_doc", id); + } + SnapshotId snapshotId = new SnapshotId("test", "test"); + IndexId indexId = new IndexId(shard.shardId().getIndexName(), shard.shardId().getIndex().getUUID()); + SourceOnlySnapshotRepository repository = new SourceOnlySnapshotRepository(createRepository()); + repository.start(); + try (Engine.IndexCommitRef snapshotRef = shard.acquireLastIndexCommit(true)) { + IndexShardSnapshotStatus indexShardSnapshotStatus = IndexShardSnapshotStatus.newInitializing(); + IllegalStateException illegalStateException = expectThrows(IllegalStateException.class, () -> + runAsSnapshot(shard.getThreadPool(), + () -> repository.snapshotShard(shard, shard.store(), snapshotId, indexId, + snapshotRef.getIndexCommit(), indexShardSnapshotStatus))); + assertEquals("Can't snapshot _source only on an index that has incomplete source ie. has _source disabled or filters the source" + , illegalStateException.getMessage()); + } + closeShards(shard); + } + + public void testIncrementalSnapshot() throws IOException { + IndexShard shard = newStartedShard(); + for (int i = 0; i < 10; i++) { + final String id = Integer.toString(i); + indexDoc(shard, "_doc", id); + } + + IndexId indexId = new IndexId(shard.shardId().getIndexName(), shard.shardId().getIndex().getUUID()); + SourceOnlySnapshotRepository repository = new SourceOnlySnapshotRepository(createRepository()); + repository.start(); + int totalFileCount = -1; + try (Engine.IndexCommitRef snapshotRef = shard.acquireLastIndexCommit(true)) { + IndexShardSnapshotStatus indexShardSnapshotStatus = IndexShardSnapshotStatus.newInitializing(); + SnapshotId snapshotId = new SnapshotId("test", "test"); + runAsSnapshot(shard.getThreadPool(), () -> repository.snapshotShard(shard, shard.store(), snapshotId, indexId, snapshotRef + .getIndexCommit(), indexShardSnapshotStatus)); + IndexShardSnapshotStatus.Copy copy = indexShardSnapshotStatus.asCopy(); + assertEquals(copy.getTotalFileCount(), copy.getIncrementalFileCount()); + totalFileCount = copy.getTotalFileCount(); + assertEquals(copy.getStage(), IndexShardSnapshotStatus.Stage.DONE); + } + + indexDoc(shard, "_doc", Integer.toString(10)); + indexDoc(shard, "_doc", Integer.toString(11)); + try (Engine.IndexCommitRef snapshotRef = shard.acquireLastIndexCommit(true)) { + SnapshotId snapshotId = new SnapshotId("test_1", "test_1"); + + IndexShardSnapshotStatus indexShardSnapshotStatus = IndexShardSnapshotStatus.newInitializing(); + runAsSnapshot(shard.getThreadPool(), () -> repository.snapshotShard(shard, shard.store(), snapshotId, indexId, snapshotRef + .getIndexCommit(), indexShardSnapshotStatus)); + IndexShardSnapshotStatus.Copy copy = indexShardSnapshotStatus.asCopy(); + // we processed the segments_N file plus _1.si, _1.fdx, _1.fnm, _1.fdt + assertEquals(5, copy.getIncrementalFileCount()); + // in total we have 4 more files than the previous snap since we don't count the segments_N twice + assertEquals(totalFileCount+4, copy.getTotalFileCount()); + assertEquals(copy.getStage(), IndexShardSnapshotStatus.Stage.DONE); + } + deleteDoc(shard, "_doc", Integer.toString(10)); + try (Engine.IndexCommitRef snapshotRef = shard.acquireLastIndexCommit(true)) { + SnapshotId snapshotId = new SnapshotId("test_2", "test_2"); + + IndexShardSnapshotStatus indexShardSnapshotStatus = IndexShardSnapshotStatus.newInitializing(); + runAsSnapshot(shard.getThreadPool(), () -> repository.snapshotShard(shard, shard.store(), snapshotId, indexId, snapshotRef + .getIndexCommit(), indexShardSnapshotStatus)); + IndexShardSnapshotStatus.Copy copy = indexShardSnapshotStatus.asCopy(); + // we processed the segments_N file plus _1_1.liv + assertEquals(2, copy.getIncrementalFileCount()); + // in total we have 5 more files than the previous snap since we don't count the segments_N twice + assertEquals(totalFileCount+5, copy.getTotalFileCount()); + assertEquals(copy.getStage(), IndexShardSnapshotStatus.Stage.DONE); + } + closeShards(shard); + } + + private String randomDoc() { + return "{ \"value\" : \"" + randomAlphaOfLength(10) + "\"}"; + } + + public void testRestoreMinmal() throws IOException { + IndexShard shard = newStartedShard(true); + int numInitialDocs = randomIntBetween(10, 100); + for (int i = 0; i < numInitialDocs; i++) { + final String id = Integer.toString(i); + indexDoc(shard, "_doc", id, randomDoc()); + if (randomBoolean()) { + shard.refresh("test"); + } + } + for (int i = 0; i < numInitialDocs; i++) { + final String id = Integer.toString(i); + if (randomBoolean()) { + if (rarely()) { + deleteDoc(shard, "_doc", id); + } else { + indexDoc(shard, "_doc", id, randomDoc()); + } + } + if (frequently()) { + shard.refresh("test"); + } + } + SnapshotId snapshotId = new SnapshotId("test", "test"); + IndexId indexId = new IndexId(shard.shardId().getIndexName(), shard.shardId().getIndex().getUUID()); + SourceOnlySnapshotRepository repository = new SourceOnlySnapshotRepository(createRepository()); + repository.start(); + try (Engine.IndexCommitRef snapshotRef = shard.acquireLastIndexCommit(true)) { + IndexShardSnapshotStatus indexShardSnapshotStatus = IndexShardSnapshotStatus.newInitializing(); + runAsSnapshot(shard.getThreadPool(), () -> { + repository.initializeSnapshot(snapshotId, Arrays.asList(indexId), + MetaData.builder().put(shard.indexSettings() + .getIndexMetaData(), false).build()); + repository.snapshotShard(shard, shard.store(), snapshotId, indexId, snapshotRef.getIndexCommit(), indexShardSnapshotStatus); + }); + IndexShardSnapshotStatus.Copy copy = indexShardSnapshotStatus.asCopy(); + assertEquals(copy.getTotalFileCount(), copy.getIncrementalFileCount()); + assertEquals(copy.getStage(), IndexShardSnapshotStatus.Stage.DONE); + } + shard.refresh("test"); + ShardRouting shardRouting = TestShardRouting.newShardRouting(new ShardId("index", "_na_", 0), randomAlphaOfLength(10), true, + ShardRoutingState.INITIALIZING, + new RecoverySource.SnapshotRecoverySource(new Snapshot("src_only", snapshotId), Version.CURRENT, indexId.getId())); + IndexMetaData metaData = runAsSnapshot(threadPool, () -> repository.getSnapshotIndexMetaData(snapshotId, indexId)); + IndexShard restoredShard = newShard(shardRouting, metaData, null, SourceOnlySnapshotRepository.getEngineFactory(), () -> {}); + restoredShard.mapperService().merge(shard.indexSettings().getIndexMetaData(), MapperService.MergeReason.MAPPING_RECOVERY); + DiscoveryNode discoveryNode = new DiscoveryNode("node_g", buildNewFakeTransportAddress(), Version.CURRENT); + restoredShard.markAsRecovering("test from snap", new RecoveryState(restoredShard.routingEntry(), discoveryNode, null)); + runAsSnapshot(shard.getThreadPool(), () -> + assertTrue(restoredShard.restoreFromRepository(repository))); + assertEquals(restoredShard.recoveryState().getStage(), RecoveryState.Stage.DONE); + assertEquals(restoredShard.recoveryState().getTranslog().recoveredOperations(), 0); + assertEquals(IndexShardState.POST_RECOVERY, restoredShard.state()); + restoredShard.refresh("test"); + assertEquals(restoredShard.docStats().getCount(), shard.docStats().getCount()); + EngineException engineException = expectThrows(EngineException.class, () -> restoredShard.get( + new Engine.Get(false, false, "_doc", Integer.toString(0), new Term("_id", Uid.encodeId(Integer.toString(0)))))); + assertEquals(engineException.getCause().getMessage(), "_source only indices can't be searched or filtered"); + SeqNoStats seqNoStats = restoredShard.seqNoStats(); + assertEquals(seqNoStats.getMaxSeqNo(), seqNoStats.getLocalCheckpoint()); + final IndexShard targetShard; + try (Engine.Searcher searcher = restoredShard.acquireSearcher("test")) { + assertEquals(searcher.reader().maxDoc(), seqNoStats.getLocalCheckpoint()); + TopDocs search = searcher.searcher().search(new MatchAllDocsQuery(), Integer.MAX_VALUE); + assertEquals(searcher.reader().numDocs(), search.totalHits.value); + search = searcher.searcher().search(new MatchAllDocsQuery(), Integer.MAX_VALUE, + new Sort(new SortField(SeqNoFieldMapper.NAME, SortField.Type.LONG)), false); + assertEquals(searcher.reader().numDocs(), search.totalHits.value); + long previous = -1; + for (ScoreDoc doc : search.scoreDocs) { + FieldDoc fieldDoc = (FieldDoc) doc; + assertEquals(1, fieldDoc.fields.length); + long current = (Long)fieldDoc.fields[0]; + assertThat(previous, Matchers.lessThan(current)); + previous = current; + } + expectThrows(UnsupportedOperationException.class, () -> searcher.searcher().search(new TermQuery(new Term("boom", "boom")), 1)); + targetShard = reindex(searcher.getDirectoryReader(), new MappingMetaData("_doc", + restoredShard.mapperService().documentMapper("_doc").meta())); + } + + for (int i = 0; i < numInitialDocs; i++) { + Engine.Get get = new Engine.Get(false, false, "_doc", Integer.toString(i), new Term("_id", Uid.encodeId(Integer.toString(i)))); + Engine.GetResult original = shard.get(get); + Engine.GetResult restored = targetShard.get(get); + assertEquals(original.exists(), restored.exists()); + + if (original.exists()) { + Document document = original.docIdAndVersion().reader.document(original.docIdAndVersion().docId); + Document restoredDocument = restored.docIdAndVersion().reader.document(restored.docIdAndVersion().docId); + for (IndexableField field : document) { + assertEquals(document.get(field.name()), restoredDocument.get(field.name())); + } + } + IOUtils.close(original, restored); + } + + closeShards(shard, restoredShard, targetShard); + } + + public IndexShard reindex(DirectoryReader reader, MappingMetaData mapping) throws IOException { + ShardRouting targetShardRouting = TestShardRouting.newShardRouting(new ShardId("target", "_na_", 0), randomAlphaOfLength(10), true, + ShardRoutingState.INITIALIZING, RecoverySource.EmptyStoreRecoverySource.INSTANCE); + Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) + .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0) + .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) + .build(); + IndexMetaData.Builder metaData = IndexMetaData.builder(targetShardRouting.getIndexName()) + .settings(settings) + .primaryTerm(0, primaryTerm); + metaData.putMapping(mapping); + IndexShard targetShard = newShard(targetShardRouting, metaData.build(), new InternalEngineFactory()); + boolean success = false; + try { + recoverShardFromStore(targetShard); + String index = targetShard.shardId().getIndexName(); + FieldsVisitor rootFieldsVisitor = new FieldsVisitor(true); + for (LeafReaderContext ctx : reader.leaves()) { + LeafReader leafReader = ctx.reader(); + Bits liveDocs = leafReader.getLiveDocs(); + for (int i = 0; i < leafReader.maxDoc(); i++) { + if (liveDocs == null || liveDocs.get(i)) { + rootFieldsVisitor.reset(); + leafReader.document(i, rootFieldsVisitor); + rootFieldsVisitor.postProcess(targetShard.mapperService()); + Uid uid = rootFieldsVisitor.uid(); + BytesReference source = rootFieldsVisitor.source(); + assert source != null : "_source is null but should have been filtered out at snapshot time"; + Engine.Result result = targetShard.applyIndexOperationOnPrimary(Versions.MATCH_ANY, VersionType.INTERNAL, source + (index, uid.type(), uid.id(), source, XContentHelper.xContentType(source)) + .routing(rootFieldsVisitor.routing()), 1, false); + if (result.getResultType() != Engine.Result.Type.SUCCESS) { + throw new IllegalStateException("failed applying post restore operation result: " + result + .getResultType(), result.getFailure()); + } + } + } + } + targetShard.refresh("test"); + success = true; + } finally { + if (success == false) { + closeShards(targetShard); + } + } + return targetShard; + } + + + /** Create a {@link Environment} with random path.home and path.repo **/ + private Environment createEnvironment() { + Path home = createTempDir(); + return TestEnvironment.newEnvironment(Settings.builder() + .put(Environment.PATH_HOME_SETTING.getKey(), home.toAbsolutePath()) + .put(Environment.PATH_REPO_SETTING.getKey(), home.resolve("repo").toAbsolutePath()) + .build()); + } + + /** Create a {@link Repository} with a random name **/ + private Repository createRepository() throws IOException { + Settings settings = Settings.builder().put("location", randomAlphaOfLength(10)).build(); + RepositoryMetaData repositoryMetaData = new RepositoryMetaData(randomAlphaOfLength(10), FsRepository.TYPE, settings); + return new FsRepository(repositoryMetaData, createEnvironment(), xContentRegistry()); + } + + private static void runAsSnapshot(ThreadPool pool, Runnable runnable) { + runAsSnapshot(pool, (Callable) () -> { + runnable.run(); + return null; + }); + } + + private static T runAsSnapshot(ThreadPool pool, Callable runnable) { + PlainActionFuture future = new PlainActionFuture<>(); + pool.executor(ThreadPool.Names.SNAPSHOT).execute(() -> { + try { + future.onResponse(runnable.call()); + } catch (Exception e) { + future.onFailure(e); + } + }); + try { + return future.get(); + } catch (ExecutionException e) { + if (e.getCause() instanceof Exception) { + throw ExceptionsHelper.convertToRuntime((Exception) e.getCause()); + } else { + throw new AssertionError(e.getCause()); + } + } catch (InterruptedException e) { + throw new AssertionError(e); + } + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotTests.java new file mode 100644 index 00000000000..e7d731739de --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotTests.java @@ -0,0 +1,245 @@ +/* + * 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.snapshots; + +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.FloatPoint; +import org.apache.lucene.document.NumericDocValuesField; +import org.apache.lucene.document.StoredField; +import org.apache.lucene.document.StringField; +import org.apache.lucene.document.TextField; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.FilterMergePolicy; +import org.apache.lucene.index.IndexCommit; +import org.apache.lucene.index.IndexFileNames; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.index.KeepOnlyLastCommitDeletionPolicy; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.index.NoMergePolicy; +import org.apache.lucene.index.RandomIndexWriter; +import org.apache.lucene.index.SegmentCommitInfo; +import org.apache.lucene.index.SegmentInfos; +import org.apache.lucene.index.SegmentReader; +import org.apache.lucene.index.SnapshotDeletionPolicy; +import org.apache.lucene.index.SoftDeletesDirectoryReaderWrapper; +import org.apache.lucene.index.StandardDirectoryReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.DocValuesFieldExistsQuery; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.store.Directory; +import org.elasticsearch.common.lucene.Lucene; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.List; + +public class SourceOnlySnapshotTests extends ESTestCase { + public void testSourceOnlyRandom() throws IOException { + try (Directory dir = newDirectory(); Directory targetDir = newDirectory()) { + SnapshotDeletionPolicy deletionPolicy = new SnapshotDeletionPolicy(new KeepOnlyLastCommitDeletionPolicy()); + IndexWriterConfig indexWriterConfig = newIndexWriterConfig().setIndexDeletionPolicy + (deletionPolicy).setSoftDeletesField(random().nextBoolean() ? null : Lucene.SOFT_DELETES_FIELD); + try (RandomIndexWriter writer = new RandomIndexWriter(random(), dir, indexWriterConfig, false)) { + final String softDeletesField = writer.w.getConfig().getSoftDeletesField(); + // we either use the soft deletes directly or manually delete them to test the additional delete functionality + boolean modifyDeletedDocs = softDeletesField != null && randomBoolean(); + SourceOnlySnapshot snapshoter = new SourceOnlySnapshot(targetDir, + modifyDeletedDocs ? () -> new DocValuesFieldExistsQuery(softDeletesField) : null) { + @Override + DirectoryReader wrapReader(DirectoryReader reader) throws IOException { + return modifyDeletedDocs ? reader : super.wrapReader(reader); + } + }; + writer.commit(); + int numDocs = scaledRandomIntBetween(100, 10000); + boolean appendOnly = randomBoolean(); + for (int i = 0; i < numDocs; i++) { + int docId = appendOnly ? i : randomIntBetween(0, 100); + Document d = newRandomDocument(docId); + if (appendOnly) { + writer.addDocument(d); + } else { + writer.updateDocument(new Term("id", Integer.toString(docId)), d); + } + if (rarely()) { + if (randomBoolean()) { + writer.commit(); + } + IndexCommit snapshot = deletionPolicy.snapshot(); + try { + snapshoter.syncSnapshot(snapshot); + } finally { + deletionPolicy.release(snapshot); + } + } + } + if (randomBoolean()) { + writer.commit(); + } + IndexCommit snapshot = deletionPolicy.snapshot(); + try { + snapshoter.syncSnapshot(snapshot); + try (DirectoryReader snapReader = snapshoter.wrapReader(DirectoryReader.open(targetDir)); + DirectoryReader wrappedReader = snapshoter.wrapReader(DirectoryReader.open(snapshot))) { + DirectoryReader reader = modifyDeletedDocs + ? new SoftDeletesDirectoryReaderWrapper(wrappedReader, softDeletesField) : wrappedReader; + assertEquals(snapReader.maxDoc(), reader.maxDoc()); + assertEquals(snapReader.numDocs(), reader.numDocs()); + for (int i = 0; i < snapReader.maxDoc(); i++) { + assertEquals(snapReader.document(i).get("_source"), reader.document(i).get("_source")); + } + for (LeafReaderContext ctx : snapReader.leaves()) { + if (ctx.reader() instanceof SegmentReader) { + assertNull(((SegmentReader) ctx.reader()).getSegmentInfo().info.getIndexSort()); + } + } + } + } finally { + deletionPolicy.release(snapshot); + } + } + } + } + + private Document newRandomDocument(int id) { + Document doc = new Document(); + doc.add(new StringField("id", Integer.toString(id), Field.Store.YES)); + doc.add(new NumericDocValuesField("id", id)); + if (randomBoolean()) { + doc.add(new TextField("text", "the quick brown fox", Field.Store.NO)); + } + if (randomBoolean()) { + doc.add(new FloatPoint("float_point", 1.3f, 3.4f)); + } + if (randomBoolean()) { + doc.add(new NumericDocValuesField("some_value", randomLong())); + } + doc.add(new StoredField("_source", randomRealisticUnicodeOfCodepointLengthBetween(5, 10))); + return doc; + } + + public void testSrcOnlySnap() throws IOException { + try (Directory dir = newDirectory()) { + SnapshotDeletionPolicy deletionPolicy = new SnapshotDeletionPolicy(new KeepOnlyLastCommitDeletionPolicy()); + IndexWriter writer = new IndexWriter(dir, newIndexWriterConfig() + .setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) + .setIndexDeletionPolicy(deletionPolicy).setMergePolicy(new FilterMergePolicy(NoMergePolicy.INSTANCE) { + @Override + public boolean useCompoundFile(SegmentInfos infos, SegmentCommitInfo mergedInfo, MergeContext mergeContext) { + return randomBoolean(); + } + })); + Document doc = new Document(); + doc.add(new StringField("id", "1", Field.Store.YES)); + doc.add(new TextField("text", "the quick brown fox", Field.Store.NO)); + doc.add(new NumericDocValuesField("rank", 1)); + doc.add(new StoredField("src", "the quick brown fox")); + writer.addDocument(doc); + doc = new Document(); + doc.add(new StringField("id", "2", Field.Store.YES)); + doc.add(new TextField("text", "the quick blue fox", Field.Store.NO)); + doc.add(new NumericDocValuesField("rank", 2)); + doc.add(new StoredField("src", "the quick blue fox")); + doc.add(new StoredField("dummy", "foo")); // add a field only this segment has + writer.addDocument(doc); + writer.flush(); + doc = new Document(); + doc.add(new StringField("id", "1", Field.Store.YES)); + doc.add(new TextField("text", "the quick brown fox", Field.Store.NO)); + doc.add(new NumericDocValuesField("rank", 3)); + doc.add(new StoredField("src", "the quick brown fox")); + writer.softUpdateDocument(new Term("id", "1"), doc, new NumericDocValuesField(Lucene.SOFT_DELETES_FIELD, 1)); + writer.commit(); + Directory targetDir = newDirectory(); + IndexCommit snapshot = deletionPolicy.snapshot(); + SourceOnlySnapshot snapshoter = new SourceOnlySnapshot(targetDir); + snapshoter.syncSnapshot(snapshot); + + StandardDirectoryReader reader = (StandardDirectoryReader) DirectoryReader.open(snapshot); + try (DirectoryReader snapReader = DirectoryReader.open(targetDir)) { + assertEquals(snapReader.maxDoc(), 3); + assertEquals(snapReader.numDocs(), 2); + for (int i = 0; i < 3; i++) { + assertEquals(snapReader.document(i).get("src"), reader.document(i).get("src")); + } + IndexSearcher searcher = new IndexSearcher(snapReader); + TopDocs id = searcher.search(new TermQuery(new Term("id", "1")), 10); + assertEquals(0, id.totalHits.value); + } + + snapshoter = new SourceOnlySnapshot(targetDir); + List createdFiles = snapshoter.syncSnapshot(snapshot); + assertEquals(0, createdFiles.size()); + deletionPolicy.release(snapshot); + // now add another doc + doc = new Document(); + doc.add(new StringField("id", "4", Field.Store.YES)); + doc.add(new TextField("text", "the quick blue fox", Field.Store.NO)); + doc.add(new NumericDocValuesField("rank", 2)); + doc.add(new StoredField("src", "the quick blue fox")); + writer.addDocument(doc); + doc = new Document(); + doc.add(new StringField("id", "5", Field.Store.YES)); + doc.add(new TextField("text", "the quick blue fox", Field.Store.NO)); + doc.add(new NumericDocValuesField("rank", 2)); + doc.add(new StoredField("src", "the quick blue fox")); + writer.addDocument(doc); + writer.commit(); + { + snapshot = deletionPolicy.snapshot(); + snapshoter = new SourceOnlySnapshot(targetDir); + createdFiles = snapshoter.syncSnapshot(snapshot); + assertEquals(4, createdFiles.size()); + for (String file : createdFiles) { + String extension = IndexFileNames.getExtension(file); + switch (extension) { + case "fdt": + case "fdx": + case "fnm": + case "si": + break; + default: + fail("unexpected extension: " + extension); + } + } + try(DirectoryReader snapReader = DirectoryReader.open(targetDir)) { + assertEquals(snapReader.maxDoc(), 5); + assertEquals(snapReader.numDocs(), 4); + } + deletionPolicy.release(snapshot); + } + writer.deleteDocuments(new Term("id", "5")); + writer.commit(); + { + snapshot = deletionPolicy.snapshot(); + snapshoter = new SourceOnlySnapshot(targetDir); + createdFiles = snapshoter.syncSnapshot(snapshot); + assertEquals(1, createdFiles.size()); + for (String file : createdFiles) { + String extension = IndexFileNames.getExtension(file); + switch (extension) { + case "liv": + break; + default: + fail("unexpected extension: " + extension); + } + } + try(DirectoryReader snapReader = DirectoryReader.open(targetDir)) { + assertEquals(snapReader.maxDoc(), 5); + assertEquals(snapReader.numDocs(), 3); + } + deletionPolicy.release(snapshot); + } + writer.close(); + targetDir.close(); + reader.close(); + } + } +} diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/snapshot/10_basic.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/snapshot/10_basic.yml new file mode 100644 index 00000000000..c0f161472b7 --- /dev/null +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/snapshot/10_basic.yml @@ -0,0 +1,84 @@ +--- +setup: + + - do: + snapshot.create_repository: + repository: test_repo_restore_1 + body: + type: source + settings: + delegate_type: fs + location: "test_repo_restore_1_loc" + + - do: + indices.create: + index: test_index + body: + settings: + number_of_shards: 1 + number_of_replicas: 0 + + - do: + cluster.health: + wait_for_status: green + +--- +"Create a source only snapshot and then restore it": + + - do: + index: + index: test_index + type: _doc + id: 1 + body: { foo: bar } + - do: + indices.flush: + index: test_index + + - do: + snapshot.create: + repository: test_repo_restore_1 + snapshot: test_snapshot + wait_for_completion: true + + - match: { snapshot.snapshot: test_snapshot } + - match: { snapshot.state : SUCCESS } + - match: { snapshot.shards.successful: 1 } + - match: { snapshot.shards.failed : 0 } + - is_true: snapshot.version + - gt: { snapshot.version_id: 0} + + - do: + indices.close: + index : test_index + + - do: + snapshot.restore: + repository: test_repo_restore_1 + snapshot: test_snapshot + wait_for_completion: true + + - do: + indices.recovery: + index: test_index + + - match: { test_index.shards.0.type: SNAPSHOT } + - match: { test_index.shards.0.stage: DONE } + - match: { test_index.shards.0.translog.recovered: 0} + - match: { test_index.shards.0.translog.total: 0} + - match: { test_index.shards.0.translog.total_on_start: 0} + - match: { test_index.shards.0.index.files.recovered: 5} + - match: { test_index.shards.0.index.files.reused: 0} + - match: { test_index.shards.0.index.size.reused_in_bytes: 0} + - gt: { test_index.shards.0.index.size.recovered_in_bytes: 0} + + - do: + search: + index: test_index + body: + query: + match_all: {} + + - match: {hits.total: 1 } + - length: {hits.hits: 1 } + - match: {hits.hits.0._id: "1" } From 901d8035d9243d2136944809ef65087e6dbc652c Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Wed, 12 Sep 2018 19:36:17 +0200 Subject: [PATCH 36/78] [CCR] Update es monitoring mapping and (#33635) * [CCR] Update es monitoring mapping and change qa tests to query based on leader index. Co-authored-by: Jason Tedor --- .../xpack/ccr/FollowIndexSecurityIT.java | 14 ++-- .../xpack/ccr/FollowIndexIT.java | 14 ++-- .../src/main/resources/monitoring-es.json | 77 +++++++++++++++++++ 3 files changed, 89 insertions(+), 16 deletions(-) diff --git a/x-pack/plugin/ccr/qa/multi-cluster-with-security/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java b/x-pack/plugin/ccr/qa/multi-cluster-with-security/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java index 26389302ece..35774443ded 100644 --- a/x-pack/plugin/ccr/qa/multi-cluster-with-security/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java +++ b/x-pack/plugin/ccr/qa/multi-cluster-with-security/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java @@ -29,6 +29,7 @@ import java.util.Map; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.is; @@ -209,22 +210,19 @@ public class FollowIndexSecurityIT extends ESRestTestCase { ensureYellow(".monitoring-*"); Request request = new Request("GET", "/.monitoring-*/_search"); - request.setJsonEntity("{\"query\": {\"term\": {\"type\": \"ccr_stats\"}}}"); + request.setJsonEntity("{\"query\": {\"term\": {\"ccr_stats.leader_index\": \"leader_cluster:" + expectedLeaderIndex + "\"}}}"); Map response = toMap(adminClient().performRequest(request)); - int numDocs = (int) XContentMapValues.extractValue("hits.total", response); - assertThat(numDocs, greaterThanOrEqualTo(1)); - int numberOfOperationsReceived = 0; int numberOfOperationsIndexed = 0; List hits = (List) XContentMapValues.extractValue("hits.hits", response); - for (int i = 0; i < numDocs; i++) { + assertThat(hits.size(), greaterThanOrEqualTo(1)); + + for (int i = 0; i < hits.size(); i++) { Map hit = (Map) hits.get(i); String leaderIndex = (String) XContentMapValues.extractValue("_source.ccr_stats.leader_index", hit); - if (leaderIndex.endsWith(expectedLeaderIndex) == false) { - continue; - } + assertThat(leaderIndex, endsWith(leaderIndex)); int foundNumberOfOperationsReceived = (int) XContentMapValues.extractValue("_source.ccr_stats.operations_received", hit); diff --git a/x-pack/plugin/ccr/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexIT.java b/x-pack/plugin/ccr/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexIT.java index 0e56084e10c..ccb5e409e8c 100644 --- a/x-pack/plugin/ccr/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexIT.java +++ b/x-pack/plugin/ccr/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexIT.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.Map; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThanOrEqualTo; @@ -162,22 +163,19 @@ public class FollowIndexIT extends ESRestTestCase { ensureYellow(".monitoring-*"); Request request = new Request("GET", "/.monitoring-*/_search"); - request.setJsonEntity("{\"query\": {\"term\": {\"type\": \"ccr_stats\"}}}"); + request.setJsonEntity("{\"query\": {\"term\": {\"ccr_stats.leader_index\": \"leader_cluster:" + expectedLeaderIndex + "\"}}}"); Map response = toMap(client().performRequest(request)); - int numDocs = (int) XContentMapValues.extractValue("hits.total", response); - assertThat(numDocs, greaterThanOrEqualTo(1)); - int numberOfOperationsReceived = 0; int numberOfOperationsIndexed = 0; List hits = (List) XContentMapValues.extractValue("hits.hits", response); - for (int i = 0; i < numDocs; i++) { + assertThat(hits.size(), greaterThanOrEqualTo(1)); + + for (int i = 0; i < hits.size(); i++) { Map hit = (Map) hits.get(i); String leaderIndex = (String) XContentMapValues.extractValue("_source.ccr_stats.leader_index", hit); - if (leaderIndex.endsWith(expectedLeaderIndex) == false) { - continue; - } + assertThat(leaderIndex, endsWith(leaderIndex)); int foundNumberOfOperationsReceived = (int) XContentMapValues.extractValue("_source.ccr_stats.operations_received", hit); diff --git a/x-pack/plugin/core/src/main/resources/monitoring-es.json b/x-pack/plugin/core/src/main/resources/monitoring-es.json index a1726a7a74a..9cca4a6e248 100644 --- a/x-pack/plugin/core/src/main/resources/monitoring-es.json +++ b/x-pack/plugin/core/src/main/resources/monitoring-es.json @@ -916,6 +916,83 @@ } } } + }, + "ccr_stats": { + "properties": { + "leader_index": { + "type": "keyword" + }, + "shard_id": { + "type": "integer" + }, + "leader_global_checkpoint": { + "type": "long" + }, + "leader_max_seq_no": { + "type": "long" + }, + "follower_global_checkpoint": { + "type": "long" + }, + "follower_max_seq_no": { + "type": "long" + }, + "last_requested_seq_no": { + "type": "long" + }, + "number_of_concurrent_reads": { + "type": "long" + }, + "number_of_concurrent_writes": { + "type": "long" + }, + "number_of_queued_writes": { + "type": "long" + }, + "mapping_version": { + "type": "long" + }, + "total_fetch_time_millis": { + "type": "long" + }, + "number_of_successful_fetches": { + "type": "long" + }, + "number_of_failed_fetches": { + "type": "long" + }, + "operations_received": { + "type": "long" + }, + "total_transferred_bytes": { + "type": "long" + }, + "total_index_time_millis": { + "type": "long" + }, + "number_of_successful_bulk_operations": { + "type": "long" + }, + "number_of_failed_bulk_operations": { + "type": "long" + }, + "number_of_operations_indexed": { + "type": "long" + }, + "fetch_exceptions": { + "properties": { + "from_seq_no": { + "type": "long" + }, + "exception": { + "type": "text" + } + } + }, + "time_since_last_fetch_millis": { + "type": "long" + } + } } } } From c023f67c5d843bb951dd2473f33806dff06819eb Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Wed, 12 Sep 2018 13:37:11 -0400 Subject: [PATCH 37/78] Add migration note for remote cluster settings (#33632) The remote cluster settings search.remote.* have been renamed to cluster.remote.* and are automatically upgraded in the cluster state on gateway recovery, and on put. This commit adds a note to the migration docs for these changes. --- .../migration/migrate_7_0/settings.asciidoc | 13 +++++++++++++ .../elasticsearch/transport/RemoteClusterAware.java | 5 +++++ .../transport/RemoteClusterService.java | 5 +++++ 3 files changed, 23 insertions(+) diff --git a/docs/reference/migration/migrate_7_0/settings.asciidoc b/docs/reference/migration/migrate_7_0/settings.asciidoc index 7826afc05fa..9fb875da382 100644 --- a/docs/reference/migration/migrate_7_0/settings.asciidoc +++ b/docs/reference/migration/migrate_7_0/settings.asciidoc @@ -40,3 +40,16 @@ will be removed in the future, thus requiring HTTP to always be enabled. This setting has been removed, as disabling http pipelining support on the server provided little value. The setting `http.pipelining.max_events` can still be used to limit the number of pipelined requests in-flight. + +==== Cross-cluster search settings renamed + +The cross-cluster search remote cluster connection infrastructure is also used +in cross-cluster replication. This means that the setting names +`search.remote.*` used for configuring cross-cluster search belie the fact that +they also apply to other situations where a connection to a remote cluster as +used. Therefore, these settings have been renamed from `search.remote.*` to +`cluster.remote.*`. For backwards compatibility purposes, we will fallback to +`search.remote.*` if `cluster.remote.*` is not set. For any such settings stored +in the cluster state, or set on dynamic settings updates, we will automatically +upgrade the setting from `search.remote.*` to `cluster.remote.*`. The fallback +settings will be removed in 8.0.0. diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteClusterAware.java b/server/src/main/java/org/elasticsearch/transport/RemoteClusterAware.java index 1c87af4a442..f75d01a0233 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteClusterAware.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteClusterAware.java @@ -52,6 +52,11 @@ import java.util.stream.Stream; */ public abstract class RemoteClusterAware extends AbstractComponent { + static { + // remove search.remote.* settings in 8.0.0 + assert Version.CURRENT.major < 8; + } + public static final Setting.AffixSetting> SEARCH_REMOTE_CLUSTERS_SEEDS = Setting.affixKeySetting( "search.remote.", diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java b/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java index 04cb1ab3e56..75891ef820c 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java @@ -66,6 +66,11 @@ import static org.elasticsearch.common.settings.Setting.boolSetting; */ public final class RemoteClusterService extends RemoteClusterAware implements Closeable { + static { + // remove search.remote.* settings in 8.0.0 + assert Version.CURRENT.major < 8; + } + public static final Setting SEARCH_REMOTE_CONNECTIONS_PER_CLUSTER = Setting.intSetting("search.remote.connections_per_cluster", 3, 1, Setting.Property.NodeScope, Setting.Property.Deprecated); From 5fa81310cc1ce4506f93c984791a586a01fde4a9 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Wed, 12 Sep 2018 19:42:00 +0200 Subject: [PATCH 38/78] [CCR] Added history uuid validation (#33546) For correctness we need to verify whether the history uuid of the leader index shards never changes while that index is being followed. * The history UUIDs are recorded as custom index metadata in the follow index. * The follow api validates whether the current history UUIDs of the leader index shards are the same as the recorded history UUIDs. If not the follow api fails. * While a follow index is following a leader index; shard follow tasks on each shard changes api call verify whether their current history uuid is the same as the recorded history uuid. Relates to #30086 Co-authored-by: Nhat Nguyen --- .../ESIndexLevelReplicationTestCase.java | 4 + .../xpack/ccr/FollowIndexSecurityIT.java | 2 +- .../java/org/elasticsearch/xpack/ccr/Ccr.java | 2 + .../xpack/ccr/CcrLicenseChecker.java | 93 +++++++++++++++--- .../xpack/ccr/action/ShardChangesAction.java | 33 ++++++- .../xpack/ccr/action/ShardFollowTask.java | 46 +++++++-- .../ccr/action/ShardFollowTasksExecutor.java | 3 +- .../TransportCreateAndFollowIndexAction.java | 21 +++- .../action/TransportFollowIndexAction.java | 51 ++++++++-- .../xpack/ccr/ShardChangesIT.java | 22 ++--- .../ccr/action/ShardChangesActionTests.java | 23 +++-- .../ccr/action/ShardChangesRequestTests.java | 5 +- .../ccr/action/ShardChangesResponseTests.java | 7 +- .../ShardFollowNodeTaskRandomTests.java | 46 +++++++-- .../ccr/action/ShardFollowNodeTaskTests.java | 35 +++++-- .../ShardFollowTaskReplicationTests.java | 71 ++++++++++++-- .../ccr/action/ShardFollowTaskTests.java | 4 +- .../TransportFollowIndexActionTests.java | 96 ++++++++++++------- 18 files changed, 442 insertions(+), 122 deletions(-) diff --git a/test/framework/src/main/java/org/elasticsearch/index/replication/ESIndexLevelReplicationTestCase.java b/test/framework/src/main/java/org/elasticsearch/index/replication/ESIndexLevelReplicationTestCase.java index 8717d7ba146..5f0909db0d3 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/replication/ESIndexLevelReplicationTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/index/replication/ESIndexLevelReplicationTestCase.java @@ -443,6 +443,10 @@ public abstract class ESIndexLevelReplicationTestCase extends IndexShardTestCase return primary; } + public synchronized void reinitPrimaryShard() throws IOException { + primary = reinitShard(primary); + } + public void syncGlobalCheckpoint() { PlainActionFuture listener = new PlainActionFuture<>(); try { diff --git a/x-pack/plugin/ccr/qa/multi-cluster-with-security/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java b/x-pack/plugin/ccr/qa/multi-cluster-with-security/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java index 35774443ded..d1a90a6ccec 100644 --- a/x-pack/plugin/ccr/qa/multi-cluster-with-security/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java +++ b/x-pack/plugin/ccr/qa/multi-cluster-with-security/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java @@ -113,7 +113,7 @@ public class FollowIndexSecurityIT extends ESRestTestCase { e = expectThrows(ResponseException.class, () -> followIndex("leader_cluster:" + unallowedIndex, unallowedIndex)); - assertThat(e.getMessage(), containsString("follow index [" + unallowedIndex + "] does not exist")); + assertThat(e.getMessage(), containsString("action [indices:monitor/stats] is unauthorized for user [test_ccr]")); assertThat(indexExists(adminClient(), unallowedIndex), is(false)); assertBusy(() -> assertThat(countCcrNodeTasks(), equalTo(0))); } diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java index 72782f6e0fe..6220ec07e4b 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java @@ -85,6 +85,8 @@ import static org.elasticsearch.xpack.core.XPackSettings.CCR_ENABLED_SETTING; public class Ccr extends Plugin implements ActionPlugin, PersistentTaskPlugin, EnginePlugin { public static final String CCR_THREAD_POOL_NAME = "ccr"; + public static final String CCR_CUSTOM_METADATA_KEY = "ccr"; + public static final String CCR_CUSTOM_METADATA_LEADER_INDEX_SHARD_HISTORY_UUIDS = "leader_index_shard_history_uuids"; private final boolean enabled; private final Settings settings; diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/CcrLicenseChecker.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/CcrLicenseChecker.java index f9a5d8fe830..2161d0a1423 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/CcrLicenseChecker.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/CcrLicenseChecker.java @@ -10,9 +10,18 @@ import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.admin.cluster.state.ClusterStateRequest; import org.elasticsearch.action.admin.cluster.state.ClusterStateResponse; +import org.elasticsearch.action.admin.indices.stats.IndexShardStats; +import org.elasticsearch.action.admin.indices.stats.IndexStats; +import org.elasticsearch.action.admin.indices.stats.IndicesStatsRequest; +import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse; +import org.elasticsearch.action.admin.indices.stats.ShardStats; import org.elasticsearch.client.Client; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.common.CheckedConsumer; +import org.elasticsearch.index.engine.CommitStats; +import org.elasticsearch.index.engine.Engine; +import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.license.RemoteClusterLicenseChecker; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.rest.RestStatus; @@ -21,6 +30,7 @@ import org.elasticsearch.xpack.core.XPackPlugin; import java.util.Collections; import java.util.Locale; import java.util.Objects; +import java.util.function.BiConsumer; import java.util.function.BooleanSupplier; import java.util.function.Consumer; import java.util.function.Function; @@ -58,23 +68,24 @@ public final class CcrLicenseChecker { } /** - * Fetches the leader index metadata from the remote cluster. Before fetching the index metadata, the remote cluster is checked for - * license compatibility with CCR. If the remote cluster is not licensed for CCR, the {@code onFailure} consumer is is invoked. - * Otherwise, the specified consumer is invoked with the leader index metadata fetched from the remote cluster. + * Fetches the leader index metadata and history UUIDs for leader index shards from the remote cluster. + * Before fetching the index metadata, the remote cluster is checked for license compatibility with CCR. + * If the remote cluster is not licensed for CCR, the {@code onFailure} consumer is is invoked. Otherwise, + * the specified consumer is invoked with the leader index metadata fetched from the remote cluster. * - * @param client the client - * @param clusterAlias the remote cluster alias - * @param leaderIndex the name of the leader index - * @param onFailure the failure consumer - * @param leaderIndexMetadataConsumer the leader index metadata consumer - * @param the type of response the listener is waiting for + * @param client the client + * @param clusterAlias the remote cluster alias + * @param leaderIndex the name of the leader index + * @param onFailure the failure consumer + * @param consumer the consumer for supplying the leader index metadata and historyUUIDs of all leader shards + * @param the type of response the listener is waiting for */ - public void checkRemoteClusterLicenseAndFetchLeaderIndexMetadata( + public void checkRemoteClusterLicenseAndFetchLeaderIndexMetadataAndHistoryUUIDs( final Client client, final String clusterAlias, final String leaderIndex, final Consumer onFailure, - final Consumer leaderIndexMetadataConsumer) { + final BiConsumer consumer) { final ClusterStateRequest request = new ClusterStateRequest(); request.clear(); @@ -85,7 +96,13 @@ public final class CcrLicenseChecker { clusterAlias, request, onFailure, - leaderClusterState -> leaderIndexMetadataConsumer.accept(leaderClusterState.getMetaData().index(leaderIndex)), + leaderClusterState -> { + IndexMetaData leaderIndexMetaData = leaderClusterState.getMetaData().index(leaderIndex); + final Client leaderClient = client.getRemoteClusterClient(clusterAlias); + fetchLeaderHistoryUUIDs(leaderClient, leaderIndexMetaData, onFailure, historyUUIDs -> { + consumer.accept(historyUUIDs, leaderIndexMetaData); + }); + }, licenseCheck -> indexMetadataNonCompliantRemoteLicense(leaderIndex, licenseCheck), e -> indexMetadataUnknownRemoteLicense(leaderIndex, clusterAlias, e)); } @@ -168,6 +185,58 @@ public final class CcrLicenseChecker { }); } + /** + * Fetches the history UUIDs for leader index on per shard basis using the specified leaderClient. + * + * @param leaderClient the leader client + * @param leaderIndexMetaData the leader index metadata + * @param onFailure the failure consumer + * @param historyUUIDConsumer the leader index history uuid and consumer + */ + // NOTE: Placed this method here; in order to avoid duplication of logic for fetching history UUIDs + // in case of following a local or a remote cluster. + public void fetchLeaderHistoryUUIDs( + final Client leaderClient, + final IndexMetaData leaderIndexMetaData, + final Consumer onFailure, + final Consumer historyUUIDConsumer) { + + String leaderIndex = leaderIndexMetaData.getIndex().getName(); + CheckedConsumer indicesStatsHandler = indicesStatsResponse -> { + IndexStats indexStats = indicesStatsResponse.getIndices().get(leaderIndex); + String[] historyUUIDs = new String[leaderIndexMetaData.getNumberOfShards()]; + for (IndexShardStats indexShardStats : indexStats) { + for (ShardStats shardStats : indexShardStats) { + // Ignore replica shards as they may not have yet started and + // we just end up overwriting slots in historyUUIDs + if (shardStats.getShardRouting().primary() == false) { + continue; + } + + CommitStats commitStats = shardStats.getCommitStats(); + if (commitStats == null) { + onFailure.accept(new IllegalArgumentException("leader index's commit stats are missing")); + return; + } + String historyUUID = commitStats.getUserData().get(Engine.HISTORY_UUID_KEY); + ShardId shardId = shardStats.getShardRouting().shardId(); + historyUUIDs[shardId.id()] = historyUUID; + } + } + for (int i = 0; i < historyUUIDs.length; i++) { + if (historyUUIDs[i] == null) { + onFailure.accept(new IllegalArgumentException("no history uuid for [" + leaderIndex + "][" + i + "]")); + return; + } + } + historyUUIDConsumer.accept(historyUUIDs); + }; + IndicesStatsRequest request = new IndicesStatsRequest(); + request.clear(); + request.indices(leaderIndex); + leaderClient.admin().indices().stats(request, ActionListener.wrap(indicesStatsHandler, onFailure)); + } + private static ElasticsearchStatusException indexMetadataNonCompliantRemoteLicense( final String leaderIndex, final RemoteClusterLicenseChecker.LicenseCheck licenseCheck) { final String clusterAlias = licenseCheck.remoteClusterLicenseInfo().clusterAlias(); diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardChangesAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardChangesAction.java index b6f82783a56..eef3671d516 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardChangesAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardChangesAction.java @@ -58,11 +58,13 @@ public class ShardChangesAction extends Action { private long fromSeqNo; private int maxOperationCount; private ShardId shardId; + private String expectedHistoryUUID; private long maxOperationSizeInBytes = FollowIndexAction.DEFAULT_MAX_BATCH_SIZE_IN_BYTES; - public Request(ShardId shardId) { + public Request(ShardId shardId, String expectedHistoryUUID) { super(shardId.getIndexName()); this.shardId = shardId; + this.expectedHistoryUUID = expectedHistoryUUID; } Request() { @@ -96,6 +98,10 @@ public class ShardChangesAction extends Action { this.maxOperationSizeInBytes = maxOperationSizeInBytes; } + public String getExpectedHistoryUUID() { + return expectedHistoryUUID; + } + @Override public ActionRequestValidationException validate() { ActionRequestValidationException validationException = null; @@ -119,6 +125,7 @@ public class ShardChangesAction extends Action { fromSeqNo = in.readVLong(); maxOperationCount = in.readVInt(); shardId = ShardId.readShardId(in); + expectedHistoryUUID = in.readString(); maxOperationSizeInBytes = in.readVLong(); } @@ -128,6 +135,7 @@ public class ShardChangesAction extends Action { out.writeVLong(fromSeqNo); out.writeVInt(maxOperationCount); shardId.writeTo(out); + out.writeString(expectedHistoryUUID); out.writeVLong(maxOperationSizeInBytes); } @@ -140,12 +148,13 @@ public class ShardChangesAction extends Action { return fromSeqNo == request.fromSeqNo && maxOperationCount == request.maxOperationCount && Objects.equals(shardId, request.shardId) && + Objects.equals(expectedHistoryUUID, request.expectedHistoryUUID) && maxOperationSizeInBytes == request.maxOperationSizeInBytes; } @Override public int hashCode() { - return Objects.hash(fromSeqNo, maxOperationCount, shardId, maxOperationSizeInBytes); + return Objects.hash(fromSeqNo, maxOperationCount, shardId, expectedHistoryUUID, maxOperationSizeInBytes); } @Override @@ -154,6 +163,7 @@ public class ShardChangesAction extends Action { "fromSeqNo=" + fromSeqNo + ", maxOperationCount=" + maxOperationCount + ", shardId=" + shardId + + ", expectedHistoryUUID=" + expectedHistoryUUID + ", maxOperationSizeInBytes=" + maxOperationSizeInBytes + '}'; } @@ -189,7 +199,12 @@ public class ShardChangesAction extends Action { Response() { } - Response(final long mappingVersion, final long globalCheckpoint, final long maxSeqNo, final Translog.Operation[] operations) { + Response( + final long mappingVersion, + final long globalCheckpoint, + final long maxSeqNo, + final Translog.Operation[] operations) { + this.mappingVersion = mappingVersion; this.globalCheckpoint = globalCheckpoint; this.maxSeqNo = maxSeqNo; @@ -260,6 +275,7 @@ public class ShardChangesAction extends Action { seqNoStats.getGlobalCheckpoint(), request.fromSeqNo, request.maxOperationCount, + request.expectedHistoryUUID, request.maxOperationSizeInBytes); return new Response(mappingVersion, seqNoStats.getGlobalCheckpoint(), seqNoStats.getMaxSeqNo(), operations); } @@ -293,11 +309,20 @@ public class ShardChangesAction extends Action { * Also if the sum of collected operations' size is above the specified maxOperationSizeInBytes then this method * stops collecting more operations and returns what has been collected so far. */ - static Translog.Operation[] getOperations(IndexShard indexShard, long globalCheckpoint, long fromSeqNo, int maxOperationCount, + static Translog.Operation[] getOperations(IndexShard indexShard, + long globalCheckpoint, + long fromSeqNo, + int maxOperationCount, + String expectedHistoryUUID, long maxOperationSizeInBytes) throws IOException { if (indexShard.state() != IndexShardState.STARTED) { throw new IndexShardNotStartedException(indexShard.shardId(), indexShard.state()); } + final String historyUUID = indexShard.getHistoryUUID(); + if (historyUUID.equals(expectedHistoryUUID) == false) { + throw new IllegalStateException("unexpected history uuid, expected [" + expectedHistoryUUID + "], actual [" + + historyUUID + "]"); + } if (fromSeqNo > globalCheckpoint) { return EMPTY_OPERATIONS_ARRAY; } diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowTask.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowTask.java index 9da19cb1998..62894b0ed99 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowTask.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowTask.java @@ -50,12 +50,13 @@ public class ShardFollowTask implements XPackPlugin.XPackPersistentTaskParams { public static final ParseField MAX_WRITE_BUFFER_SIZE = new ParseField("max_write_buffer_size"); public static final ParseField MAX_RETRY_DELAY = new ParseField("max_retry_delay"); public static final ParseField IDLE_SHARD_RETRY_DELAY = new ParseField("idle_shard_retry_delay"); + public static final ParseField RECORDED_HISTORY_UUID = new ParseField("recorded_history_uuid"); @SuppressWarnings("unchecked") private static ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, (a) -> new ShardFollowTask((String) a[0], new ShardId((String) a[1], (String) a[2], (int) a[3]), new ShardId((String) a[4], (String) a[5], (int) a[6]), (int) a[7], (int) a[8], (long) a[9], - (int) a[10], (int) a[11], (TimeValue) a[12], (TimeValue) a[13], (Map) a[14])); + (int) a[10], (int) a[11], (TimeValue) a[12], (TimeValue) a[13], (String) a[14], (Map) a[15])); static { PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), LEADER_CLUSTER_ALIAS_FIELD); @@ -76,6 +77,7 @@ public class ShardFollowTask implements XPackPlugin.XPackPersistentTaskParams { PARSER.declareField(ConstructingObjectParser.constructorArg(), (p, c) -> TimeValue.parseTimeValue(p.text(), IDLE_SHARD_RETRY_DELAY.getPreferredName()), IDLE_SHARD_RETRY_DELAY, ObjectParser.ValueType.STRING); + PARSER.declareString(ConstructingObjectParser.constructorArg(), RECORDED_HISTORY_UUID); PARSER.declareObject(ConstructingObjectParser.constructorArg(), (p, c) -> p.mapStrings(), HEADERS); } @@ -89,11 +91,22 @@ public class ShardFollowTask implements XPackPlugin.XPackPersistentTaskParams { private final int maxWriteBufferSize; private final TimeValue maxRetryDelay; private final TimeValue idleShardRetryDelay; + private final String recordedLeaderIndexHistoryUUID; private final Map headers; - ShardFollowTask(String leaderClusterAlias, ShardId followShardId, ShardId leaderShardId, int maxBatchOperationCount, - int maxConcurrentReadBatches, long maxBatchSizeInBytes, int maxConcurrentWriteBatches, - int maxWriteBufferSize, TimeValue maxRetryDelay, TimeValue idleShardRetryDelay, Map headers) { + ShardFollowTask( + String leaderClusterAlias, + ShardId followShardId, + ShardId leaderShardId, + int maxBatchOperationCount, + int maxConcurrentReadBatches, + long maxBatchSizeInBytes, + int maxConcurrentWriteBatches, + int maxWriteBufferSize, + TimeValue maxRetryDelay, + TimeValue idleShardRetryDelay, + String recordedLeaderIndexHistoryUUID, + Map headers) { this.leaderClusterAlias = leaderClusterAlias; this.followShardId = followShardId; this.leaderShardId = leaderShardId; @@ -104,6 +117,7 @@ public class ShardFollowTask implements XPackPlugin.XPackPersistentTaskParams { this.maxWriteBufferSize = maxWriteBufferSize; this.maxRetryDelay = maxRetryDelay; this.idleShardRetryDelay = idleShardRetryDelay; + this.recordedLeaderIndexHistoryUUID = recordedLeaderIndexHistoryUUID; this.headers = headers != null ? Collections.unmodifiableMap(headers) : Collections.emptyMap(); } @@ -118,6 +132,7 @@ public class ShardFollowTask implements XPackPlugin.XPackPersistentTaskParams { this.maxWriteBufferSize = in.readVInt(); this.maxRetryDelay = in.readTimeValue(); this.idleShardRetryDelay = in.readTimeValue(); + this.recordedLeaderIndexHistoryUUID = in.readString(); this.headers = Collections.unmodifiableMap(in.readMap(StreamInput::readString, StreamInput::readString)); } @@ -165,6 +180,10 @@ public class ShardFollowTask implements XPackPlugin.XPackPersistentTaskParams { return followShardId.getIndex().getUUID() + "-" + followShardId.getId(); } + public String getRecordedLeaderIndexHistoryUUID() { + return recordedLeaderIndexHistoryUUID; + } + public Map getHeaders() { return headers; } @@ -186,6 +205,7 @@ public class ShardFollowTask implements XPackPlugin.XPackPersistentTaskParams { out.writeVInt(maxWriteBufferSize); out.writeTimeValue(maxRetryDelay); out.writeTimeValue(idleShardRetryDelay); + out.writeString(recordedLeaderIndexHistoryUUID); out.writeMap(headers, StreamOutput::writeString, StreamOutput::writeString); } @@ -212,6 +232,7 @@ public class ShardFollowTask implements XPackPlugin.XPackPersistentTaskParams { builder.field(MAX_WRITE_BUFFER_SIZE.getPreferredName(), maxWriteBufferSize); builder.field(MAX_RETRY_DELAY.getPreferredName(), maxRetryDelay.getStringRep()); builder.field(IDLE_SHARD_RETRY_DELAY.getPreferredName(), idleShardRetryDelay.getStringRep()); + builder.field(RECORDED_HISTORY_UUID.getPreferredName(), recordedLeaderIndexHistoryUUID); builder.field(HEADERS.getPreferredName(), headers); return builder.endObject(); } @@ -231,13 +252,26 @@ public class ShardFollowTask implements XPackPlugin.XPackPersistentTaskParams { maxWriteBufferSize == that.maxWriteBufferSize && Objects.equals(maxRetryDelay, that.maxRetryDelay) && Objects.equals(idleShardRetryDelay, that.idleShardRetryDelay) && + Objects.equals(recordedLeaderIndexHistoryUUID, that.recordedLeaderIndexHistoryUUID) && Objects.equals(headers, that.headers); } @Override public int hashCode() { - return Objects.hash(leaderClusterAlias, followShardId, leaderShardId, maxBatchOperationCount, maxConcurrentReadBatches, - maxConcurrentWriteBatches, maxBatchSizeInBytes, maxWriteBufferSize, maxRetryDelay, idleShardRetryDelay, headers); + return Objects.hash( + leaderClusterAlias, + followShardId, + leaderShardId, + maxBatchOperationCount, + maxConcurrentReadBatches, + maxConcurrentWriteBatches, + maxBatchSizeInBytes, + maxWriteBufferSize, + maxRetryDelay, + idleShardRetryDelay, + recordedLeaderIndexHistoryUUID, + headers + ); } public String toString() { diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowTasksExecutor.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowTasksExecutor.java index 83e3e4806e1..7b63e73ee59 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowTasksExecutor.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowTasksExecutor.java @@ -133,7 +133,8 @@ public class ShardFollowTasksExecutor extends PersistentTasksExecutor handler, Consumer errorHandler) { - ShardChangesAction.Request request = new ShardChangesAction.Request(params.getLeaderShardId()); + ShardChangesAction.Request request = + new ShardChangesAction.Request(params.getLeaderShardId(), params.getRecordedLeaderIndexHistoryUUID()); request.setFromSeqNo(from); request.setMaxOperationCount(maxOperationCount); request.setMaxOperationSizeInBytes(params.getMaxBatchSizeInBytes()); diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportCreateAndFollowIndexAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportCreateAndFollowIndexAction.java index b99b569a525..c6d1a7c36c5 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportCreateAndFollowIndexAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportCreateAndFollowIndexAction.java @@ -33,14 +33,17 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.RemoteClusterAware; import org.elasticsearch.transport.RemoteClusterService; import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.ccr.Ccr; import org.elasticsearch.xpack.ccr.CcrLicenseChecker; import org.elasticsearch.xpack.ccr.CcrSettings; import org.elasticsearch.xpack.core.ccr.action.CreateAndFollowIndexAction; import org.elasticsearch.xpack.core.ccr.action.FollowIndexAction; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.function.Consumer; public final class TransportCreateAndFollowIndexAction extends TransportMasterNodeAction { @@ -116,8 +119,12 @@ public final class TransportCreateAndFollowIndexAction final ClusterState state, final ActionListener listener) { // following an index in local cluster, so use local cluster state to fetch leader index metadata - final IndexMetaData leaderIndexMetadata = state.getMetaData().index(request.getFollowRequest().getLeaderIndex()); - createFollowerIndex(leaderIndexMetadata, request, listener); + final String leaderIndex = request.getFollowRequest().getLeaderIndex(); + final IndexMetaData leaderIndexMetadata = state.getMetaData().index(leaderIndex); + Consumer handler = historyUUIDs -> { + createFollowerIndex(leaderIndexMetadata, historyUUIDs, request, listener); + }; + ccrLicenseChecker.fetchLeaderHistoryUUIDs(client, leaderIndexMetadata, listener::onFailure, handler); } private void createFollowerIndexAndFollowRemoteIndex( @@ -125,16 +132,17 @@ public final class TransportCreateAndFollowIndexAction final String clusterAlias, final String leaderIndex, final ActionListener listener) { - ccrLicenseChecker.checkRemoteClusterLicenseAndFetchLeaderIndexMetadata( + ccrLicenseChecker.checkRemoteClusterLicenseAndFetchLeaderIndexMetadataAndHistoryUUIDs( client, clusterAlias, leaderIndex, listener::onFailure, - leaderIndexMetaData -> createFollowerIndex(leaderIndexMetaData, request, listener)); + (historyUUID, leaderIndexMetaData) -> createFollowerIndex(leaderIndexMetaData, historyUUID, request, listener)); } private void createFollowerIndex( final IndexMetaData leaderIndexMetaData, + final String[] historyUUIDs, final CreateAndFollowIndexAction.Request request, final ActionListener listener) { if (leaderIndexMetaData == null) { @@ -172,6 +180,11 @@ public final class TransportCreateAndFollowIndexAction MetaData.Builder mdBuilder = MetaData.builder(currentState.metaData()); IndexMetaData.Builder imdBuilder = IndexMetaData.builder(followIndex); + // Adding the leader index uuid for each shard as custom metadata: + Map metadata = new HashMap<>(); + metadata.put(Ccr.CCR_CUSTOM_METADATA_LEADER_INDEX_SHARD_HISTORY_UUIDS, String.join(",", historyUUIDs)); + imdBuilder.putCustom(Ccr.CCR_CUSTOM_METADATA_KEY, metadata); + // Copy all settings, but overwrite a few settings. Settings.Builder settingsBuilder = Settings.builder(); settingsBuilder.put(leaderIndexMetaData.getSettings()); diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexAction.java index 3128a63f24b..fff3f1618aa 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexAction.java @@ -19,6 +19,7 @@ import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.IndexingSlowLog; import org.elasticsearch.index.SearchSlowLog; @@ -35,6 +36,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.RemoteClusterAware; import org.elasticsearch.transport.RemoteClusterService; import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.ccr.Ccr; import org.elasticsearch.xpack.ccr.CcrLicenseChecker; import org.elasticsearch.xpack.ccr.CcrSettings; import org.elasticsearch.xpack.core.ccr.action.FollowIndexAction; @@ -110,11 +112,16 @@ public class TransportFollowIndexAction extends HandledTransportAction { + try { + start(request, null, leaderIndexMetadata, followerIndexMetadata, historyUUIDs, listener); + } catch (final IOException e) { + listener.onFailure(e); + } + }); } private void followRemoteIndex( @@ -124,14 +131,14 @@ public class TransportFollowIndexAction extends HandledTransportAction listener) { final ClusterState state = clusterService.state(); final IndexMetaData followerIndexMetadata = state.getMetaData().index(request.getFollowerIndex()); - ccrLicenseChecker.checkRemoteClusterLicenseAndFetchLeaderIndexMetadata( + ccrLicenseChecker.checkRemoteClusterLicenseAndFetchLeaderIndexMetadataAndHistoryUUIDs( client, clusterAlias, leaderIndex, listener::onFailure, - leaderIndexMetadata -> { + (leaderHistoryUUID, leaderIndexMetadata) -> { try { - start(request, clusterAlias, leaderIndexMetadata, followerIndexMetadata, listener); + start(request, clusterAlias, leaderIndexMetadata, followerIndexMetadata, leaderHistoryUUID, listener); } catch (final IOException e) { listener.onFailure(e); } @@ -153,18 +160,23 @@ public class TransportFollowIndexAction extends HandledTransportAction handler) throws IOException { MapperService mapperService = followIndexMetadata != null ? indicesService.createIndexMapperService(followIndexMetadata) : null; - validate(request, leaderIndexMetadata, followIndexMetadata, mapperService); + validate(request, leaderIndexMetadata, followIndexMetadata, leaderIndexHistoryUUIDs, mapperService); final int numShards = followIndexMetadata.getNumberOfShards(); final AtomicInteger counter = new AtomicInteger(numShards); final AtomicReferenceArray responses = new AtomicReferenceArray<>(followIndexMetadata.getNumberOfShards()); Map filteredHeaders = threadPool.getThreadContext().getHeaders().entrySet().stream() .filter(e -> ShardFollowTask.HEADER_FILTERS.contains(e.getKey())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));for (int i = 0; i < numShards; i++) { + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + for (int i = 0; i < numShards; i++) { final int shardId = i; String taskId = followIndexMetadata.getIndexUUID() + "-" + shardId; + String[] recordedLeaderShardHistoryUUIDs = extractIndexShardHistoryUUIDs(followIndexMetadata); + String recordedLeaderShardHistoryUUID = recordedLeaderShardHistoryUUIDs[shardId]; ShardFollowTask shardFollowTask = new ShardFollowTask( clusterNameAlias, @@ -177,6 +189,7 @@ public class TransportFollowIndexAction extends HandledTransportAction>() { @@ -224,6 +237,7 @@ public class TransportFollowIndexAction extends HandledTransportAction> WHITE_LISTED_SETTINGS; static { diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/ShardChangesIT.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/ShardChangesIT.java index c0919f25fe3..f4291ddc8dd 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/ShardChangesIT.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/ShardChangesIT.java @@ -27,7 +27,9 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.support.XContentMapValues; +import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.engine.Engine; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.translog.Translog; import org.elasticsearch.persistent.PersistentTasksCustomMetaData; @@ -116,7 +118,8 @@ public class ShardChangesIT extends ESIntegTestCase { long globalCheckPoint = shardStats.getSeqNoStats().getGlobalCheckpoint(); assertThat(globalCheckPoint, equalTo(2L)); - ShardChangesAction.Request request = new ShardChangesAction.Request(shardStats.getShardRouting().shardId()); + String historyUUID = shardStats.getCommitStats().getUserData().get(Engine.HISTORY_UUID_KEY); + ShardChangesAction.Request request = new ShardChangesAction.Request(shardStats.getShardRouting().shardId(), historyUUID); request.setFromSeqNo(0L); request.setMaxOperationCount(3); ShardChangesAction.Response response = client().execute(ShardChangesAction.INSTANCE, request).get(); @@ -141,7 +144,7 @@ public class ShardChangesIT extends ESIntegTestCase { globalCheckPoint = shardStats.getSeqNoStats().getGlobalCheckpoint(); assertThat(globalCheckPoint, equalTo(5L)); - request = new ShardChangesAction.Request(shardStats.getShardRouting().shardId()); + request = new ShardChangesAction.Request(shardStats.getShardRouting().shardId(), historyUUID); request.setFromSeqNo(3L); request.setMaxOperationCount(3); response = client().execute(ShardChangesAction.INSTANCE, request).get(); @@ -357,16 +360,11 @@ public class ShardChangesIT extends ESIntegTestCase { final String leaderIndexSettings = getIndexSettingsWithNestedMapping(1, between(0, 1), singletonMap(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true")); assertAcked(client().admin().indices().prepareCreate("index1").setSource(leaderIndexSettings, XContentType.JSON)); - - final String followerIndexSettings = - getIndexSettingsWithNestedMapping(1, between(0, 1), singletonMap(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), "true")); - assertAcked(client().admin().indices().prepareCreate("index2").setSource(followerIndexSettings, XContentType.JSON)); - internalCluster().ensureAtLeastNumDataNodes(2); - ensureGreen("index1", "index2"); + ensureGreen("index1"); final FollowIndexAction.Request followRequest = createFollowRequest("index1", "index2"); - client().execute(FollowIndexAction.INSTANCE, followRequest).get(); + client().execute(CreateAndFollowIndexAction.INSTANCE, new CreateAndFollowIndexAction.Request(followRequest)).get(); final int numDocs = randomIntBetween(2, 64); for (int i = 0; i < numDocs; i++) { @@ -409,13 +407,13 @@ public class ShardChangesIT extends ESIntegTestCase { assertAcked(client().admin().indices().prepareCreate("test-follower").get()); // Leader index does not exist. FollowIndexAction.Request followRequest1 = createFollowRequest("non-existent-leader", "test-follower"); - expectThrows(IllegalArgumentException.class, () -> client().execute(FollowIndexAction.INSTANCE, followRequest1).actionGet()); + expectThrows(IndexNotFoundException.class, () -> client().execute(FollowIndexAction.INSTANCE, followRequest1).actionGet()); // Follower index does not exist. FollowIndexAction.Request followRequest2 = createFollowRequest("non-test-leader", "non-existent-follower"); - expectThrows(IllegalArgumentException.class, () -> client().execute(FollowIndexAction.INSTANCE, followRequest2).actionGet()); + expectThrows(IndexNotFoundException.class, () -> client().execute(FollowIndexAction.INSTANCE, followRequest2).actionGet()); // Both indices do not exist. FollowIndexAction.Request followRequest3 = createFollowRequest("non-existent-leader", "non-existent-follower"); - expectThrows(IllegalArgumentException.class, () -> client().execute(FollowIndexAction.INSTANCE, followRequest3).actionGet()); + expectThrows(IndexNotFoundException.class, () -> client().execute(FollowIndexAction.INSTANCE, followRequest3).actionGet()); } @TestLogging("_root:DEBUG") diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardChangesActionTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardChangesActionTests.java index 430e9cb48b1..b973fbac3ce 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardChangesActionTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardChangesActionTests.java @@ -59,22 +59,27 @@ public class ShardChangesActionTests extends ESSingleNodeTestCase { int max = randomIntBetween(min, numWrites - 1); int size = max - min + 1; final Translog.Operation[] operations = ShardChangesAction.getOperations(indexShard, - indexShard.getGlobalCheckpoint(), min, size, Long.MAX_VALUE); + indexShard.getGlobalCheckpoint(), min, size, indexShard.getHistoryUUID(), Long.MAX_VALUE); final List seenSeqNos = Arrays.stream(operations).map(Translog.Operation::seqNo).collect(Collectors.toList()); final List expectedSeqNos = LongStream.rangeClosed(min, max).boxed().collect(Collectors.toList()); assertThat(seenSeqNos, equalTo(expectedSeqNos)); } - // get operations for a range no operations exists: Translog.Operation[] operations = ShardChangesAction.getOperations(indexShard, indexShard.getGlobalCheckpoint(), - numWrites, numWrites + 1, Long.MAX_VALUE); + numWrites, numWrites + 1, indexShard.getHistoryUUID(), Long.MAX_VALUE); assertThat(operations.length, equalTo(0)); // get operations for a range some operations do not exist: operations = ShardChangesAction.getOperations(indexShard, indexShard.getGlobalCheckpoint(), - numWrites - 10, numWrites + 10, Long.MAX_VALUE); + numWrites - 10, numWrites + 10, indexShard.getHistoryUUID(), Long.MAX_VALUE); assertThat(operations.length, equalTo(10)); + + // Unexpected history UUID: + Exception e = expectThrows(IllegalStateException.class, () -> ShardChangesAction.getOperations(indexShard, + indexShard.getGlobalCheckpoint(), 0, 10, "different-history-uuid", Long.MAX_VALUE)); + assertThat(e.getMessage(), equalTo("unexpected history uuid, expected [different-history-uuid], actual [" + + indexShard.getHistoryUUID() + "]")); } public void testGetOperationsWhenShardNotStarted() throws Exception { @@ -83,7 +88,7 @@ public class ShardChangesActionTests extends ESSingleNodeTestCase { ShardRouting shardRouting = TestShardRouting.newShardRouting("index", 0, "_node_id", true, ShardRoutingState.INITIALIZING); Mockito.when(indexShard.routingEntry()).thenReturn(shardRouting); expectThrows(IndexShardNotStartedException.class, () -> ShardChangesAction.getOperations(indexShard, - indexShard.getGlobalCheckpoint(), 0, 1, Long.MAX_VALUE)); + indexShard.getGlobalCheckpoint(), 0, 1, indexShard.getHistoryUUID(), Long.MAX_VALUE)); } public void testGetOperationsExceedByteLimit() throws Exception { @@ -100,7 +105,7 @@ public class ShardChangesActionTests extends ESSingleNodeTestCase { final IndexShard indexShard = indexService.getShard(0); final Translog.Operation[] operations = ShardChangesAction.getOperations(indexShard, indexShard.getGlobalCheckpoint(), - 0, 12, 256); + 0, 12, indexShard.getHistoryUUID(), 256); assertThat(operations.length, equalTo(12)); assertThat(operations[0].seqNo(), equalTo(0L)); assertThat(operations[1].seqNo(), equalTo(1L)); @@ -127,7 +132,7 @@ public class ShardChangesActionTests extends ESSingleNodeTestCase { final IndexShard indexShard = indexService.getShard(0); final Translog.Operation[] operations = - ShardChangesAction.getOperations(indexShard, indexShard.getGlobalCheckpoint(), 0, 1, 0); + ShardChangesAction.getOperations(indexShard, indexShard.getGlobalCheckpoint(), 0, 1, indexShard.getHistoryUUID(), 0); assertThat(operations.length, equalTo(1)); assertThat(operations[0].seqNo(), equalTo(0L)); } @@ -137,7 +142,7 @@ public class ShardChangesActionTests extends ESSingleNodeTestCase { final AtomicReference reference = new AtomicReference<>(); final ShardChangesAction.TransportAction transportAction = node().injector().getInstance(ShardChangesAction.TransportAction.class); transportAction.execute( - new ShardChangesAction.Request(new ShardId(new Index("non-existent", "uuid"), 0)), + new ShardChangesAction.Request(new ShardId(new Index("non-existent", "uuid"), 0), "uuid"), new ActionListener() { @Override public void onResponse(final ShardChangesAction.Response response) { @@ -162,7 +167,7 @@ public class ShardChangesActionTests extends ESSingleNodeTestCase { final AtomicReference reference = new AtomicReference<>(); final ShardChangesAction.TransportAction transportAction = node().injector().getInstance(ShardChangesAction.TransportAction.class); transportAction.execute( - new ShardChangesAction.Request(new ShardId(indexService.getMetaData().getIndex(), numberOfShards)), + new ShardChangesAction.Request(new ShardId(indexService.getMetaData().getIndex(), numberOfShards), "uuid"), new ActionListener() { @Override public void onResponse(final ShardChangesAction.Response response) { diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardChangesRequestTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardChangesRequestTests.java index 19585da8851..2ea2086990b 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardChangesRequestTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardChangesRequestTests.java @@ -15,7 +15,8 @@ public class ShardChangesRequestTests extends AbstractStreamableTestCase scheduler = (delay, task) -> { @@ -215,8 +225,16 @@ public class ShardFollowNodeTaskRandomTests extends ESTestCase { byte[] source = "{}".getBytes(StandardCharsets.UTF_8); ops.add(new Translog.Index("doc", id, seqNo, 0, source)); } - item.add(new TestResponse(null, mappingVersion, - new ShardChangesAction.Response(mappingVersion, nextGlobalCheckPoint, nextGlobalCheckPoint, ops.toArray(EMPTY)))); + item.add(new TestResponse( + null, + mappingVersion, + new ShardChangesAction.Response( + mappingVersion, + nextGlobalCheckPoint, + nextGlobalCheckPoint, + ops.toArray(EMPTY)) + ) + ); responses.put(prevGlobalCheckpoint, item); } else { // Simulates a leader shard copy not having all the operations the shard follow task thinks it has by @@ -232,8 +250,12 @@ public class ShardFollowNodeTaskRandomTests extends ESTestCase { } // Sometimes add an empty shard changes response to also simulate a leader shard lagging behind if (sometimes()) { - ShardChangesAction.Response response = - new ShardChangesAction.Response(mappingVersion, prevGlobalCheckpoint, prevGlobalCheckpoint, EMPTY); + ShardChangesAction.Response response = new ShardChangesAction.Response( + mappingVersion, + prevGlobalCheckpoint, + prevGlobalCheckpoint, + EMPTY + ); item.add(new TestResponse(null, mappingVersion, response)); } List ops = new ArrayList<>(); @@ -244,8 +266,12 @@ public class ShardFollowNodeTaskRandomTests extends ESTestCase { } // Report toSeqNo to simulate maxBatchSizeInBytes limit being met or last op to simulate a shard lagging behind: long localLeaderGCP = randomBoolean() ? ops.get(ops.size() - 1).seqNo() : toSeqNo; - ShardChangesAction.Response response = - new ShardChangesAction.Response(mappingVersion, localLeaderGCP, localLeaderGCP, ops.toArray(EMPTY)); + ShardChangesAction.Response response = new ShardChangesAction.Response( + mappingVersion, + localLeaderGCP, + localLeaderGCP, + ops.toArray(EMPTY) + ); item.add(new TestResponse(null, mappingVersion, response)); responses.put(fromSeqNo, Collections.unmodifiableList(item)); } diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskTests.java index e25d95538b2..101b2580759 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskTests.java @@ -627,9 +627,20 @@ public class ShardFollowNodeTaskTests extends ESTestCase { int bufferWriteLimit, long maxBatchSizeInBytes) { AtomicBoolean stopped = new AtomicBoolean(false); - ShardFollowTask params = new ShardFollowTask(null, new ShardId("follow_index", "", 0), - new ShardId("leader_index", "", 0), maxBatchOperationCount, maxConcurrentReadBatches, maxBatchSizeInBytes, - maxConcurrentWriteBatches, bufferWriteLimit, TimeValue.ZERO, TimeValue.ZERO, Collections.emptyMap()); + ShardFollowTask params = new ShardFollowTask( + null, + new ShardId("follow_index", "", 0), + new ShardId("leader_index", "", 0), + maxBatchOperationCount, + maxConcurrentReadBatches, + maxBatchSizeInBytes, + maxConcurrentWriteBatches, + bufferWriteLimit, + TimeValue.ZERO, + TimeValue.ZERO, + "uuid", + Collections.emptyMap() + ); shardChangesRequests = new ArrayList<>(); bulkShardOperationRequests = new ArrayList<>(); @@ -690,12 +701,12 @@ public class ShardFollowNodeTaskTests extends ESTestCase { for (int i = 0; i < requestBatchSize; i++) { operations[i] = new Translog.NoOp(from + i, 0, "test"); } - final ShardChangesAction.Response response = - new ShardChangesAction.Response( - mappingVersions.poll(), - leaderGlobalCheckpoints.poll(), - maxSeqNos.poll(), - operations); + final ShardChangesAction.Response response = new ShardChangesAction.Response( + mappingVersions.poll(), + leaderGlobalCheckpoints.poll(), + maxSeqNos.poll(), + operations + ); handler.accept(response); } } @@ -727,7 +738,11 @@ public class ShardFollowNodeTaskTests extends ESTestCase { ops.add(new Translog.Index("doc", id, seqNo, 0, source)); } return new ShardChangesAction.Response( - mappingVersion, leaderGlobalCheckPoint, leaderGlobalCheckPoint, ops.toArray(new Translog.Operation[0])); + mappingVersion, + leaderGlobalCheckPoint, + leaderGlobalCheckPoint, + ops.toArray(new Translog.Operation[0]) + ); } void startTask(ShardFollowNodeTask task, long leaderGlobalCheckpoint, long followerGlobalCheckpoint) { diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowTaskReplicationTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowTaskReplicationTests.java index 2cd024cb03c..9b04390a3a7 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowTaskReplicationTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowTaskReplicationTests.java @@ -38,11 +38,13 @@ import java.util.Collections; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.LongConsumer; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; public class ShardFollowTaskReplicationTests extends ESIndexLevelReplicationTestCase { @@ -129,6 +131,43 @@ public class ShardFollowTaskReplicationTests extends ESIndexLevelReplicationTest } } + public void testChangeHistoryUUID() throws Exception { + try (ReplicationGroup leaderGroup = createGroup(0); + ReplicationGroup followerGroup = createFollowGroup(0)) { + leaderGroup.startAll(); + int docCount = leaderGroup.appendDocs(randomInt(64)); + leaderGroup.assertAllEqual(docCount); + followerGroup.startAll(); + ShardFollowNodeTask shardFollowTask = createShardFollowTask(leaderGroup, followerGroup); + final SeqNoStats leaderSeqNoStats = leaderGroup.getPrimary().seqNoStats(); + final SeqNoStats followerSeqNoStats = followerGroup.getPrimary().seqNoStats(); + shardFollowTask.start( + leaderSeqNoStats.getGlobalCheckpoint(), + leaderSeqNoStats.getMaxSeqNo(), + followerSeqNoStats.getGlobalCheckpoint(), + followerSeqNoStats.getMaxSeqNo()); + leaderGroup.syncGlobalCheckpoint(); + leaderGroup.assertAllEqual(docCount); + Set indexedDocIds = getShardDocUIDs(leaderGroup.getPrimary()); + assertBusy(() -> { + assertThat(followerGroup.getPrimary().getGlobalCheckpoint(), equalTo(leaderGroup.getPrimary().getGlobalCheckpoint())); + followerGroup.assertAllEqual(indexedDocIds.size()); + }); + + String oldHistoryUUID = leaderGroup.getPrimary().getHistoryUUID(); + leaderGroup.reinitPrimaryShard(); + leaderGroup.getPrimary().store().bootstrapNewHistory(); + recoverShardFromStore(leaderGroup.getPrimary()); + String newHistoryUUID = leaderGroup.getPrimary().getHistoryUUID(); + + assertBusy(() -> { + assertThat(shardFollowTask.isStopped(), is(true)); + assertThat(shardFollowTask.getFailure().getMessage(), equalTo("unexpected history uuid, expected [" + oldHistoryUUID + + "], actual [" + newHistoryUUID + "]")); + }); + } + } + @Override protected ReplicationGroup createGroup(int replicas, Settings settings) throws IOException { Settings newSettings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) @@ -159,12 +198,23 @@ public class ShardFollowTaskReplicationTests extends ESIndexLevelReplicationTest } private ShardFollowNodeTask createShardFollowTask(ReplicationGroup leaderGroup, ReplicationGroup followerGroup) { - ShardFollowTask params = new ShardFollowTask(null, new ShardId("follow_index", "", 0), - new ShardId("leader_index", "", 0), between(1, 64), between(1, 8), Long.MAX_VALUE, between(1, 4), 10240, - TimeValue.timeValueMillis(10), TimeValue.timeValueMillis(10), Collections.emptyMap()); + ShardFollowTask params = new ShardFollowTask( + null, + new ShardId("follow_index", "", 0), + new ShardId("leader_index", "", 0), + between(1, 64), + between(1, 8), + Long.MAX_VALUE, + between(1, 4), 10240, + TimeValue.timeValueMillis(10), + TimeValue.timeValueMillis(10), + leaderGroup.getPrimary().getHistoryUUID(), + Collections.emptyMap() + ); BiConsumer scheduler = (delay, task) -> threadPool.schedule(delay, ThreadPool.Names.GENERIC, task); AtomicBoolean stopped = new AtomicBoolean(false); + AtomicReference failureHolder = new AtomicReference<>(); LongSet fetchOperations = new LongHashSet(); return new ShardFollowNodeTask( 1L, "type", ShardFollowTask.NAME, "description", null, Collections.emptyMap(), params, scheduler, System::nanoTime) { @@ -210,10 +260,14 @@ public class ShardFollowTaskReplicationTests extends ESIndexLevelReplicationTest try { final SeqNoStats seqNoStats = indexShard.seqNoStats(); Translog.Operation[] ops = ShardChangesAction.getOperations(indexShard, seqNoStats.getGlobalCheckpoint(), from, - maxOperationCount, params.getMaxBatchSizeInBytes()); + maxOperationCount, params.getRecordedLeaderIndexHistoryUUID(), params.getMaxBatchSizeInBytes()); // hard code mapping version; this is ok, as mapping updates are not tested here - final ShardChangesAction.Response response = - new ShardChangesAction.Response(1L, seqNoStats.getGlobalCheckpoint(), seqNoStats.getMaxSeqNo(), ops); + final ShardChangesAction.Response response = new ShardChangesAction.Response( + 1L, + seqNoStats.getGlobalCheckpoint(), + seqNoStats.getMaxSeqNo(), + ops + ); handler.accept(response); return; } catch (Exception e) { @@ -238,9 +292,14 @@ public class ShardFollowTaskReplicationTests extends ESIndexLevelReplicationTest @Override public void markAsFailed(Exception e) { + failureHolder.set(e); stopped.set(true); } + @Override + public Exception getFailure() { + return failureHolder.get(); + } }; } diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowTaskTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowTaskTests.java index 300794a6c00..fa11ddf4bf9 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowTaskTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowTaskTests.java @@ -34,7 +34,9 @@ public class ShardFollowTaskTests extends AbstractSerializingTestCase CUSTOM_METADATA = + singletonMap(Ccr.CCR_CUSTOM_METADATA_LEADER_INDEX_SHARD_HISTORY_UUIDS, "uuid"); + public void testValidation() throws IOException { FollowIndexAction.Request request = ShardChangesIT.createFollowRequest("index1", "index2"); + String[] UUIDs = new String[]{"uuid"}; { // should fail, because leader index does not exist - Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, null, null, null)); + Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, null, null, null, null)); assertThat(e.getMessage(), equalTo("leader index [index1] does not exist")); } { // should fail, because follow index does not exist - IndexMetaData leaderIMD = createIMD("index1", 5, Settings.EMPTY); - Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, null, null)); + IndexMetaData leaderIMD = createIMD("index1", 5, Settings.EMPTY, emptyMap()); + Exception e = expectThrows(IllegalArgumentException.class, + () -> validate(request, leaderIMD, null, null, null)); assertThat(e.getMessage(), equalTo("follow index [index2] does not exist")); } + { + // should fail because the recorded leader index history uuid is not equal to the leader actual index history uuid: + IndexMetaData leaderIMD = createIMD("index1", 5, Settings.EMPTY, emptyMap()); + Map customMetaData = + singletonMap(Ccr.CCR_CUSTOM_METADATA_LEADER_INDEX_SHARD_HISTORY_UUIDS, "another-uuid"); + IndexMetaData followIMD = createIMD("index2", 5, Settings.EMPTY, customMetaData); + Exception e = expectThrows(IllegalArgumentException.class, + () -> validate(request, leaderIMD, followIMD, UUIDs, null)); + assertThat(e.getMessage(), equalTo("leader shard [index2][0] should reference [another-uuid] as history uuid but " + + "instead reference [uuid] as history uuid")); + } { // should fail because leader index does not have soft deletes enabled - IndexMetaData leaderIMD = createIMD("index1", 5, Settings.EMPTY); - IndexMetaData followIMD = createIMD("index2", 5, Settings.EMPTY); - Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, null)); + IndexMetaData leaderIMD = createIMD("index1", 5, Settings.EMPTY, emptyMap()); + IndexMetaData followIMD = createIMD("index2", 5, Settings.EMPTY, CUSTOM_METADATA); + Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, UUIDs, null)); assertThat(e.getMessage(), equalTo("leader index [index1] does not have soft deletes enabled")); } { // should fail because the number of primary shards between leader and follow index are not equal IndexMetaData leaderIMD = createIMD("index1", 5, Settings.builder() - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build()); - IndexMetaData followIMD = createIMD("index2", 4, Settings.EMPTY); - Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, null)); + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build(), emptyMap()); + IndexMetaData followIMD = createIMD("index2", 4, Settings.EMPTY, CUSTOM_METADATA); + Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, UUIDs, null)); assertThat(e.getMessage(), equalTo("leader index primary shards [5] does not match with the number of shards of the follow index [4]")); } { // should fail, because leader index is closed IndexMetaData leaderIMD = createIMD("index1", State.CLOSE, "{}", 5, Settings.builder() - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build()); + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build(), emptyMap()); IndexMetaData followIMD = createIMD("index2", State.OPEN, "{}", 5, Settings.builder() - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build()); - Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, null)); + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build(), CUSTOM_METADATA); + Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, UUIDs, null)); assertThat(e.getMessage(), equalTo("leader and follow index must be open")); } { // should fail, because leader has a field with the same name mapped as keyword and follower as text IndexMetaData leaderIMD = createIMD("index1", State.OPEN, "{\"properties\": {\"field\": {\"type\": \"keyword\"}}}", 5, - Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build()); + Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build(), emptyMap()); IndexMetaData followIMD = createIMD("index2", State.OPEN, "{\"properties\": {\"field\": {\"type\": \"text\"}}}", 5, - Settings.builder().put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true).build()); + Settings.builder().put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true).build(), CUSTOM_METADATA); MapperService mapperService = MapperTestUtils.newMapperService(xContentRegistry(), createTempDir(), Settings.EMPTY, "index2"); mapperService.updateMapping(null, followIMD); - Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, mapperService)); + Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, UUIDs, mapperService)); assertThat(e.getMessage(), equalTo("mapper [field] of different type, current_type [text], merged_type [keyword]")); } { @@ -80,38 +100,38 @@ public class TransportFollowIndexActionTests extends ESTestCase { IndexMetaData leaderIMD = createIMD("index1", State.OPEN, mapping, 5, Settings.builder() .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true") .put("index.analysis.analyzer.my_analyzer.type", "custom") - .put("index.analysis.analyzer.my_analyzer.tokenizer", "whitespace").build()); + .put("index.analysis.analyzer.my_analyzer.tokenizer", "whitespace").build(), emptyMap()); IndexMetaData followIMD = createIMD("index2", State.OPEN, mapping, 5, Settings.builder() .put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true) .put("index.analysis.analyzer.my_analyzer.type", "custom") - .put("index.analysis.analyzer.my_analyzer.tokenizer", "standard").build()); - Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, null)); + .put("index.analysis.analyzer.my_analyzer.tokenizer", "standard").build(), CUSTOM_METADATA); + Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, UUIDs, null)); assertThat(e.getMessage(), equalTo("the leader and follower index settings must be identical")); } { // should fail because the following index does not have the following_index settings IndexMetaData leaderIMD = createIMD("index1", 5, - Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build()); + Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build(), emptyMap()); Settings followingIndexSettings = randomBoolean() ? Settings.EMPTY : Settings.builder().put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), false).build(); - IndexMetaData followIMD = createIMD("index2", 5, followingIndexSettings); + IndexMetaData followIMD = createIMD("index2", 5, followingIndexSettings, CUSTOM_METADATA); MapperService mapperService = MapperTestUtils.newMapperService(xContentRegistry(), createTempDir(), followingIndexSettings, "index2"); mapperService.updateMapping(null, followIMD); IllegalArgumentException error = - expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, mapperService)); + expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, UUIDs, mapperService)); assertThat(error.getMessage(), equalTo("the following index [index2] is not ready to follow; " + "the setting [index.xpack.ccr.following_index] must be enabled.")); } { // should succeed IndexMetaData leaderIMD = createIMD("index1", 5, Settings.builder() - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build()); + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build(), emptyMap()); IndexMetaData followIMD = createIMD("index2", 5, Settings.builder() - .put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true).build()); + .put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true).build(), CUSTOM_METADATA); MapperService mapperService = MapperTestUtils.newMapperService(xContentRegistry(), createTempDir(), Settings.EMPTY, "index2"); mapperService.updateMapping(null, followIMD); - validate(request, leaderIMD, followIMD, mapperService); + validate(request, leaderIMD, followIMD, UUIDs, mapperService); } { // should succeed, index settings are identical @@ -119,15 +139,15 @@ public class TransportFollowIndexActionTests extends ESTestCase { IndexMetaData leaderIMD = createIMD("index1", State.OPEN, mapping, 5, Settings.builder() .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true") .put("index.analysis.analyzer.my_analyzer.type", "custom") - .put("index.analysis.analyzer.my_analyzer.tokenizer", "standard").build()); + .put("index.analysis.analyzer.my_analyzer.tokenizer", "standard").build(), emptyMap()); IndexMetaData followIMD = createIMD("index2", State.OPEN, mapping, 5, Settings.builder() .put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true) .put("index.analysis.analyzer.my_analyzer.type", "custom") - .put("index.analysis.analyzer.my_analyzer.tokenizer", "standard").build()); + .put("index.analysis.analyzer.my_analyzer.tokenizer", "standard").build(), CUSTOM_METADATA); MapperService mapperService = MapperTestUtils.newMapperService(xContentRegistry(), createTempDir(), followIMD.getSettings(), "index2"); mapperService.updateMapping(null, followIMD); - validate(request, leaderIMD, followIMD, mapperService); + validate(request, leaderIMD, followIMD, UUIDs, mapperService); } { // should succeed despite whitelisted settings being different @@ -136,25 +156,32 @@ public class TransportFollowIndexActionTests extends ESTestCase { .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true") .put(IndexSettings.INDEX_REFRESH_INTERVAL_SETTING.getKey(), "1s") .put("index.analysis.analyzer.my_analyzer.type", "custom") - .put("index.analysis.analyzer.my_analyzer.tokenizer", "standard").build()); + .put("index.analysis.analyzer.my_analyzer.tokenizer", "standard").build(), emptyMap()); IndexMetaData followIMD = createIMD("index2", State.OPEN, mapping, 5, Settings.builder() .put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true) .put(IndexSettings.INDEX_REFRESH_INTERVAL_SETTING.getKey(), "10s") .put("index.analysis.analyzer.my_analyzer.type", "custom") - .put("index.analysis.analyzer.my_analyzer.tokenizer", "standard").build()); + .put("index.analysis.analyzer.my_analyzer.tokenizer", "standard").build(), CUSTOM_METADATA); MapperService mapperService = MapperTestUtils.newMapperService(xContentRegistry(), createTempDir(), followIMD.getSettings(), "index2"); mapperService.updateMapping(null, followIMD); - validate(request, leaderIMD, followIMD, mapperService); + validate(request, leaderIMD, followIMD, UUIDs, mapperService); } } - private static IndexMetaData createIMD(String index, int numberOfShards, Settings settings) throws IOException { - return createIMD(index, State.OPEN, "{\"properties\": {}}", numberOfShards, settings); + private static IndexMetaData createIMD(String index, + int numberOfShards, + Settings settings, + Map custom) throws IOException { + return createIMD(index, State.OPEN, "{\"properties\": {}}", numberOfShards, settings, custom); } - private static IndexMetaData createIMD(String index, State state, String mapping, int numberOfShards, - Settings settings) throws IOException { + private static IndexMetaData createIMD(String index, + State state, + String mapping, + int numberOfShards, + Settings settings, + Map custom) throws IOException { return IndexMetaData.builder(index) .settings(settings(Version.CURRENT).put(settings)) .numberOfShards(numberOfShards) @@ -162,6 +189,7 @@ public class TransportFollowIndexActionTests extends ESTestCase { .numberOfReplicas(0) .setRoutingNumShards(numberOfShards) .putMapping("_doc", mapping) + .putCustom(Ccr.CCR_CUSTOM_METADATA_KEY, custom) .build(); } From cfc20825ab8dea80d2e9dd3b5ff04a151602d8c1 Mon Sep 17 00:00:00 2001 From: Vladimir Dolzhenko Date: Wed, 12 Sep 2018 20:34:56 +0200 Subject: [PATCH 39/78] mute NamedDateTimeProcessorTests --- .../scalar/datetime/NamedDateTimeProcessorTests.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NamedDateTimeProcessorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NamedDateTimeProcessorTests.java index 3d57675e209..828a16f5aa9 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NamedDateTimeProcessorTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NamedDateTimeProcessorTests.java @@ -37,6 +37,7 @@ public class NamedDateTimeProcessorTests extends AbstractWireSerializingTestCase return new NamedDateTimeProcessor(replaced, UTC); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/33621") public void testValidDayNamesInUTC() { NamedDateTimeProcessor proc = new NamedDateTimeProcessor(NameExtractor.DAY_NAME, UTC); assertEquals("Thursday", proc.process("0")); @@ -48,7 +49,8 @@ public class NamedDateTimeProcessorTests extends AbstractWireSerializingTestCase assertEquals("Friday", proc.process(new DateTime(30, 2, 1, 12, 13, DateTimeZone.UTC))); assertEquals("Tuesday", proc.process(new DateTime(10902, 8, 22, 11, 11, DateTimeZone.UTC))); } - + + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/33621") public void testValidDayNamesWithNonUTCTimeZone() { NamedDateTimeProcessor proc = new NamedDateTimeProcessor(NameExtractor.DAY_NAME, TimeZone.getTimeZone("GMT-10:00")); assertEquals("Wednesday", proc.process("0")); @@ -61,7 +63,8 @@ public class NamedDateTimeProcessorTests extends AbstractWireSerializingTestCase assertEquals("Tuesday", proc.process(new DateTime(10902, 8, 22, 11, 11, DateTimeZone.UTC))); assertEquals("Monday", proc.process(new DateTime(10902, 8, 22, 9, 59, DateTimeZone.UTC))); } - + + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/33621") public void testValidMonthNamesInUTC() { NamedDateTimeProcessor proc = new NamedDateTimeProcessor(NameExtractor.MONTH_NAME, UTC); assertEquals("January", proc.process("0")); @@ -73,7 +76,8 @@ public class NamedDateTimeProcessorTests extends AbstractWireSerializingTestCase assertEquals("February", proc.process(new DateTime(30, 2, 1, 12, 13, DateTimeZone.UTC))); assertEquals("August", proc.process(new DateTime(10902, 8, 22, 11, 11, DateTimeZone.UTC))); } - + + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/33621") public void testValidMonthNamesWithNonUTCTimeZone() { NamedDateTimeProcessor proc = new NamedDateTimeProcessor(NameExtractor.MONTH_NAME, TimeZone.getTimeZone("GMT-3:00")); assertEquals("December", proc.process("0")); From 9b8fe85edb81378f16d3f0819b4f5604bad7e7cf Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Wed, 12 Sep 2018 14:38:24 -0400 Subject: [PATCH 40/78] Remove volatile from global checkpoint listeners (#33636) This field does not need to be volatile because all accesses are done under a lock. This commit removes the unnecessary volatile modifier from this field. --- .../elasticsearch/index/shard/GlobalCheckpointListeners.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java b/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java index df93a935b62..224d5be17e1 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java +++ b/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java @@ -63,7 +63,7 @@ public class GlobalCheckpointListeners implements Closeable { // guarded by this private boolean closed; - private volatile Map> listeners; + private Map> listeners; private long lastKnownGlobalCheckpoint = UNASSIGNED_SEQ_NO; private final ShardId shardId; From 20c6c9c542835f1620a495c54f2d84235f338fa7 Mon Sep 17 00:00:00 2001 From: Jay Modi Date: Wed, 12 Sep 2018 13:08:09 -0600 Subject: [PATCH 41/78] Address license state update/read thread safety (#33396) This change addresses some issues regarding thread safety around updates and method calls on the XPackLicenseState object. There exists a possibility that there could be a concurrent update to the XPackLicenseState when there is a scheduled check to see if the license is expired and a cluster state update. In order to address this, the update method now has a synchronized block where member variables are updated. Each method that reads these variables is now also synchronized. Along with the above change, there was a consistency issue around security calls to the license state. The majority of security checks make two calls to the license state, which could result in incorrect behavior due to the checks being made against different license states. The majority of this behavior was introduced for 6.3 with the inclusion of x-pack in the default distribution. In order to resolve the majority of these cases, the `isSecurityEnabled` method is no longer public and the logic is also included in individual methods about security such as `isAuthAllowed`. There were a few cases where this did not remove multiple calls on the license state, so a new method has been added which creates a copy of the current license state that will not change. Callers can use this copy of the license state to make decisions based on a consistent view of the license state. --- .../license/XPackLicenseState.java | 188 +++++++++++------- .../SecurityIndexSearcherWrapper.java | 4 +- .../license/XPackLicenseStateTests.java | 28 +-- ...yIndexSearcherWrapperIntegrationTests.java | 1 - ...SecurityIndexSearcherWrapperUnitTests.java | 1 - .../ml/action/TransportPutDatafeedAction.java | 2 +- .../xpack/security/Security.java | 2 +- .../xpack/security/SecurityFeatureSet.java | 6 +- .../action/filter/SecurityActionFilter.java | 6 +- .../BulkShardRequestInterceptor.java | 28 +-- ...cumentLevelSecurityRequestInterceptor.java | 29 ++- .../IndicesAliasesRequestInterceptor.java | 54 ++--- .../interceptor/ResizeRequestInterceptor.java | 42 ++-- .../security/audit/AuditTrailService.java | 40 ++-- .../xpack/security/authc/Realms.java | 4 +- .../SecuritySearchOperationListener.java | 4 +- .../authz/accesscontrol/OptOutQueryCache.java | 3 +- .../security/rest/SecurityRestFilter.java | 2 +- .../rest/action/SecurityBaseRestHandler.java | 17 +- .../SecurityServerTransportInterceptor.java | 4 +- .../security/transport/filter/IPFilter.java | 2 +- .../elasticsearch/license/LicensingTests.java | 2 +- .../security/SecurityFeatureSetTests.java | 5 +- .../filter/SecurityActionFilterTests.java | 1 - ...IndicesAliasesRequestInterceptorTests.java | 6 +- .../ResizeRequestInterceptorTests.java | 6 +- .../audit/AuditTrailServiceTests.java | 14 -- .../authc/AuthenticationServiceTests.java | 1 - .../xpack/security/authc/RealmsTests.java | 1 - .../SecuritySearchOperationListenerTests.java | 10 - .../accesscontrol/OptOutQueryCacheTests.java | 27 +-- .../rest/SecurityRestFilterTests.java | 1 - .../action/SecurityBaseRestHandlerTests.java | 9 +- ...curityServerTransportInterceptorTests.java | 14 +- .../transport/filter/IPFilterTests.java | 1 - .../IpFilterRemoteAddressFilterTests.java | 1 - .../transport/nio/NioIPFilterTests.java | 1 - 37 files changed, 276 insertions(+), 291 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java index 37176803d4f..1fe4ebf0850 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java @@ -265,13 +265,15 @@ public class XPackLicenseState { } } - private volatile Status status = new Status(OperationMode.TRIAL, true); - private final List listeners = new CopyOnWriteArrayList<>(); + private final List listeners; private final boolean isSecurityEnabled; private final boolean isSecurityExplicitlyEnabled; - private volatile boolean isSecurityEnabledByTrialVersion; + + private Status status = new Status(OperationMode.TRIAL, true); + private boolean isSecurityEnabledByTrialVersion; public XPackLicenseState(Settings settings) { + this.listeners = new CopyOnWriteArrayList<>(); this.isSecurityEnabled = XPackSettings.SECURITY_ENABLED.get(settings); // 6.0+ requires TLS for production licenses, so if TLS is enabled and security is enabled // we can interpret this as an explicit enabling of security if the security enabled @@ -281,6 +283,14 @@ public class XPackLicenseState { this.isSecurityEnabledByTrialVersion = false; } + private XPackLicenseState(XPackLicenseState xPackLicenseState) { + this.listeners = xPackLicenseState.listeners; + this.isSecurityEnabled = xPackLicenseState.isSecurityEnabled; + this.isSecurityExplicitlyEnabled = xPackLicenseState.isSecurityExplicitlyEnabled; + this.status = xPackLicenseState.status; + this.isSecurityEnabledByTrialVersion = xPackLicenseState.isSecurityEnabledByTrialVersion; + } + /** * Updates the current state of the license, which will change what features are available. * @@ -291,15 +301,17 @@ public class XPackLicenseState { * trial was prior to this metadata being tracked (6.1) */ void update(OperationMode mode, boolean active, @Nullable Version mostRecentTrialVersion) { - status = new Status(mode, active); - if (isSecurityEnabled == true && isSecurityExplicitlyEnabled == false && mode == OperationMode.TRIAL - && isSecurityEnabledByTrialVersion == false) { - // Before 6.3, Trial licenses would default having security enabled. - // If this license was generated before that version, then treat it as if security is explicitly enabled - if (mostRecentTrialVersion == null || mostRecentTrialVersion.before(Version.V_6_3_0)) { - Loggers.getLogger(getClass()).info("Automatically enabling security for older trial license ({})", - mostRecentTrialVersion == null ? "[pre 6.1.0]" : mostRecentTrialVersion.toString()); - isSecurityEnabledByTrialVersion = true; + synchronized (this) { + status = new Status(mode, active); + if (isSecurityEnabled == true && isSecurityExplicitlyEnabled == false && mode == OperationMode.TRIAL + && isSecurityEnabledByTrialVersion == false) { + // Before 6.3, Trial licenses would default having security enabled. + // If this license was generated before that version, then treat it as if security is explicitly enabled + if (mostRecentTrialVersion == null || mostRecentTrialVersion.before(Version.V_6_3_0)) { + Loggers.getLogger(getClass()).info("Automatically enabling security for older trial license ({})", + mostRecentTrialVersion == null ? "[pre 6.1.0]" : mostRecentTrialVersion.toString()); + isSecurityEnabledByTrialVersion = true; + } } } listeners.forEach(Runnable::run); @@ -316,12 +328,12 @@ public class XPackLicenseState { } /** Return the current license type. */ - public OperationMode getOperationMode() { + public synchronized OperationMode getOperationMode() { return status.mode; } /** Return true if the license is currently within its time boundaries, false otherwise. */ - public boolean isActive() { + public synchronized boolean isActive() { return status.active; } @@ -329,28 +341,32 @@ public class XPackLicenseState { * @return true if authentication and authorization should be enabled. this does not indicate what realms are available * @see #allowedRealmType() for the enabled realms */ - public boolean isAuthAllowed() { + public synchronized boolean isAuthAllowed() { OperationMode mode = status.mode; - return mode == OperationMode.STANDARD || mode == OperationMode.GOLD || mode == OperationMode.PLATINUM - || mode == OperationMode.TRIAL; + final boolean isSecurityCurrentlyEnabled = + isSecurityEnabled(mode, isSecurityExplicitlyEnabled, isSecurityEnabledByTrialVersion, isSecurityEnabled); + return isSecurityCurrentlyEnabled && (mode == OperationMode.STANDARD || mode == OperationMode.GOLD + || mode == OperationMode.PLATINUM || mode == OperationMode.TRIAL); } /** * @return true if IP filtering should be enabled */ - public boolean isIpFilteringAllowed() { + public synchronized boolean isIpFilteringAllowed() { OperationMode mode = status.mode; - return mode == OperationMode.GOLD || mode == OperationMode.PLATINUM - || mode == OperationMode.TRIAL; + final boolean isSecurityCurrentlyEnabled = + isSecurityEnabled(mode, isSecurityExplicitlyEnabled, isSecurityEnabledByTrialVersion, isSecurityEnabled); + return isSecurityCurrentlyEnabled && (mode == OperationMode.GOLD || mode == OperationMode.PLATINUM || mode == OperationMode.TRIAL); } /** * @return true if auditing should be enabled */ - public boolean isAuditingAllowed() { + public synchronized boolean isAuditingAllowed() { OperationMode mode = status.mode; - return mode == OperationMode.GOLD || mode == OperationMode.PLATINUM - || mode == OperationMode.TRIAL; + final boolean isSecurityCurrentlyEnabled = + isSecurityEnabled(mode, isSecurityExplicitlyEnabled, isSecurityEnabledByTrialVersion, isSecurityEnabled); + return isSecurityCurrentlyEnabled && (mode == OperationMode.GOLD || mode == OperationMode.PLATINUM || mode == OperationMode.TRIAL); } /** @@ -359,7 +375,7 @@ public class XPackLicenseState { * * @return true if the license allows for the stats and health APIs to be used. */ - public boolean isStatsAndHealthAllowed() { + public synchronized boolean isStatsAndHealthAllowed() { return status.active; } @@ -375,9 +391,11 @@ public class XPackLicenseState { * * @return {@code true} to enable DLS and FLS. Otherwise {@code false}. */ - public boolean isDocumentAndFieldLevelSecurityAllowed() { + public synchronized boolean isDocumentAndFieldLevelSecurityAllowed() { OperationMode mode = status.mode; - return mode == OperationMode.TRIAL || mode == OperationMode.PLATINUM; + final boolean isSecurityCurrentlyEnabled = + isSecurityEnabled(mode, isSecurityExplicitlyEnabled, isSecurityEnabledByTrialVersion, isSecurityEnabled); + return isSecurityCurrentlyEnabled && (mode == OperationMode.TRIAL || mode == OperationMode.PLATINUM); } /** Classes of realms that may be available based on the license type. */ @@ -391,37 +409,45 @@ public class XPackLicenseState { /** * @return the type of realms that are enabled based on the license {@link OperationMode} */ - public AllowedRealmType allowedRealmType() { - switch (status.mode) { - case PLATINUM: - case TRIAL: - return AllowedRealmType.ALL; - case GOLD: - return AllowedRealmType.DEFAULT; - case STANDARD: - return AllowedRealmType.NATIVE; - default: - return AllowedRealmType.NONE; + public synchronized AllowedRealmType allowedRealmType() { + final boolean isSecurityCurrentlyEnabled = + isSecurityEnabled(status.mode, isSecurityExplicitlyEnabled, isSecurityEnabledByTrialVersion, isSecurityEnabled); + if (isSecurityCurrentlyEnabled) { + switch (status.mode) { + case PLATINUM: + case TRIAL: + return AllowedRealmType.ALL; + case GOLD: + return AllowedRealmType.DEFAULT; + case STANDARD: + return AllowedRealmType.NATIVE; + default: + return AllowedRealmType.NONE; + } + } else { + return AllowedRealmType.NONE; } } /** * @return whether custom role providers are allowed based on the license {@link OperationMode} */ - public boolean isCustomRoleProvidersAllowed() { - final Status localStatus = status; - return (localStatus.mode == OperationMode.PLATINUM || localStatus.mode == OperationMode.TRIAL) - && localStatus.active; + public synchronized boolean isCustomRoleProvidersAllowed() { + final boolean isSecurityCurrentlyEnabled = + isSecurityEnabled(status.mode, isSecurityExplicitlyEnabled, isSecurityEnabledByTrialVersion, isSecurityEnabled); + return isSecurityCurrentlyEnabled && (status.mode == OperationMode.PLATINUM || status.mode == OperationMode.TRIAL) + && status.active; } /** * @return whether "authorization_realms" are allowed based on the license {@link OperationMode} * @see org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings */ - public boolean isAuthorizationRealmAllowed() { - final Status localStatus = status; - return (localStatus.mode == OperationMode.PLATINUM || localStatus.mode == OperationMode.TRIAL) - && localStatus.active; + public synchronized boolean isAuthorizationRealmAllowed() { + final boolean isSecurityCurrentlyEnabled = + isSecurityEnabled(status.mode, isSecurityExplicitlyEnabled, isSecurityEnabledByTrialVersion, isSecurityEnabled); + return isSecurityCurrentlyEnabled && (status.mode == OperationMode.PLATINUM || status.mode == OperationMode.TRIAL) + && status.active; } /** @@ -437,8 +463,7 @@ public class XPackLicenseState { * * @return {@code true} as long as the license is valid. Otherwise {@code false}. */ - public boolean isWatcherAllowed() { - // status is volatile, so a local variable is used for a consistent view + public synchronized boolean isWatcherAllowed() { Status localStatus = status; if (localStatus.active == false) { @@ -461,7 +486,7 @@ public class XPackLicenseState { * * @return true if the license is active */ - public boolean isMonitoringAllowed() { + public synchronized boolean isMonitoringAllowed() { return status.active; } @@ -471,7 +496,7 @@ public class XPackLicenseState { * @return {@link #isWatcherAllowed()} * @see #isWatcherAllowed() */ - public boolean isMonitoringClusterAlertsAllowed() { + public synchronized boolean isMonitoringClusterAlertsAllowed() { return isWatcherAllowed(); } @@ -484,7 +509,7 @@ public class XPackLicenseState { * * @return {@code true} if the user is allowed to modify the retention. Otherwise {@code false}. */ - public boolean isUpdateRetentionAllowed() { + public synchronized boolean isUpdateRetentionAllowed() { final OperationMode mode = status.mode; return mode != OperationMode.BASIC && mode != OperationMode.MISSING; } @@ -500,8 +525,7 @@ public class XPackLicenseState { * * @return {@code true} as long as the license is valid. Otherwise {@code false}. */ - public boolean isGraphAllowed() { - // status is volatile + public synchronized boolean isGraphAllowed() { Status localStatus = status; OperationMode operationMode = localStatus.mode; @@ -523,8 +547,7 @@ public class XPackLicenseState { * @return {@code true} as long as the license is valid. Otherwise * {@code false}. */ - public boolean isMachineLearningAllowed() { - // one-time volatile read as status could be updated on us while performing this check + public synchronized boolean isMachineLearningAllowed() { final Status currentStatus = status; return currentStatus.active && isMachineLearningAllowedForOperationMode(currentStatus.mode); } @@ -538,7 +561,7 @@ public class XPackLicenseState { * * @return true if the license is active */ - public boolean isRollupAllowed() { + public synchronized boolean isRollupAllowed() { return status.active; } @@ -546,7 +569,7 @@ public class XPackLicenseState { * Logstash is allowed as long as there is an active license of type TRIAL, STANDARD, GOLD or PLATINUM * @return {@code true} as long as there is a valid license */ - public boolean isLogstashAllowed() { + public synchronized boolean isLogstashAllowed() { Status localStatus = status; return localStatus.active && (isBasic(localStatus.mode) == false); } @@ -555,7 +578,7 @@ public class XPackLicenseState { * Beats is allowed as long as there is an active license of type TRIAL, STANDARD, GOLD or PLATINUM * @return {@code true} as long as there is a valid license */ - public boolean isBeatsAllowed() { + public synchronized boolean isBeatsAllowed() { Status localStatus = status; return localStatus.active && (isBasic(localStatus.mode) == false); @@ -565,7 +588,7 @@ public class XPackLicenseState { * Deprecation APIs are always allowed as long as there is an active license * @return {@code true} as long as there is a valid license */ - public boolean isDeprecationAllowed() { + public synchronized boolean isDeprecationAllowed() { return status.active; } @@ -577,11 +600,9 @@ public class XPackLicenseState { * @return {@code true} as long as the license is valid. Otherwise * {@code false}. */ - public boolean isUpgradeAllowed() { - // status is volatile - Status localStatus = status; + public synchronized boolean isUpgradeAllowed() { // Should work on all active licenses - return localStatus.active; + return status.active; } /** @@ -589,7 +610,7 @@ public class XPackLicenseState { *

* SQL is available for all license types except {@link OperationMode#MISSING} */ - public boolean isSqlAllowed() { + public synchronized boolean isSqlAllowed() { return status.active; } @@ -598,8 +619,7 @@ public class XPackLicenseState { *

* JDBC is available only in for {@link OperationMode#PLATINUM} and {@link OperationMode#TRIAL} licences */ - public boolean isJdbcAllowed() { - // status is volatile + public synchronized boolean isJdbcAllowed() { Status localStatus = status; OperationMode operationMode = localStatus.mode; @@ -608,18 +628,35 @@ public class XPackLicenseState { return licensed && localStatus.active; } - public boolean isTrialLicense() { + public synchronized boolean isTrialLicense() { return status.mode == OperationMode.TRIAL; } - public boolean isSecurityAvailable() { + /** + * @return true if security is available to be used with the current license type + */ + public synchronized boolean isSecurityAvailable() { OperationMode mode = status.mode; return mode == OperationMode.GOLD || mode == OperationMode.PLATINUM || mode == OperationMode.STANDARD || mode == OperationMode.TRIAL; } - public boolean isSecurityEnabled() { - final OperationMode mode = status.mode; + /** + * @return true if security has been disabled by a trial license which is the case of the + * default distribution post 6.3.0. The conditions necessary for this are: + *

    + *
  • A trial license generated in 6.3.0+
  • + *
  • xpack.security.enabled not specified as a setting
  • + *
+ */ + public synchronized boolean isSecurityDisabledByTrialLicense() { + return status.mode == OperationMode.TRIAL && isSecurityEnabled + && isSecurityExplicitlyEnabled == false + && isSecurityEnabledByTrialVersion == false; + } + + private static boolean isSecurityEnabled(final OperationMode mode, final boolean isSecurityExplicitlyEnabled, + final boolean isSecurityEnabledByTrialVersion, final boolean isSecurityEnabled) { return mode == OperationMode.TRIAL ? (isSecurityExplicitlyEnabled || isSecurityEnabledByTrialVersion) : isSecurityEnabled; } @@ -634,8 +671,7 @@ public class XPackLicenseState { * * @return true is the license is compatible, otherwise false */ - public boolean isCcrAllowed() { - // one-time volatile read as status could be updated on us while performing this check + public synchronized boolean isCcrAllowed() { final Status currentStatus = status; return currentStatus.active && isCcrAllowedForOperationMode(currentStatus.mode); } @@ -648,4 +684,14 @@ public class XPackLicenseState { return operationMode == OperationMode.PLATINUM || operationMode == OperationMode.TRIAL; } + /** + * Creates a copy of this object based on the state at the time the method was called. The + * returned object will not be modified by a license update/expiration so it can be used to + * make multiple method calls on the license state safely. This object should not be long + * lived but instead used within a method when a consistent view of the license state + * is needed for multiple interactions with the license state. + */ + public synchronized XPackLicenseState copyCurrentLicenseState() { + return new XPackLicenseState(this); + } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexSearcherWrapper.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexSearcherWrapper.java index 60b598a3a99..e0dc36b4117 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexSearcherWrapper.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexSearcherWrapper.java @@ -110,7 +110,7 @@ public class SecurityIndexSearcherWrapper extends IndexSearcherWrapper { @Override protected DirectoryReader wrap(DirectoryReader reader) { - if (licenseState.isSecurityEnabled() == false || licenseState.isDocumentAndFieldLevelSecurityAllowed() == false) { + if (licenseState.isDocumentAndFieldLevelSecurityAllowed() == false) { return reader; } @@ -171,7 +171,7 @@ public class SecurityIndexSearcherWrapper extends IndexSearcherWrapper { @Override protected IndexSearcher wrap(IndexSearcher searcher) throws EngineException { - if (licenseState.isSecurityEnabled() == false || licenseState.isDocumentAndFieldLevelSecurityAllowed() == false) { + if (licenseState.isDocumentAndFieldLevelSecurityAllowed() == false) { return searcher; } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/XPackLicenseStateTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/XPackLicenseStateTests.java index c2cb5af1305..76b735dc78a 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/XPackLicenseStateTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/XPackLicenseStateTests.java @@ -92,13 +92,13 @@ public class XPackLicenseStateTests extends ESTestCase { assertThat(licenseState.isCustomRoleProvidersAllowed(), is(true)); licenseState = new XPackLicenseState(Settings.EMPTY); - assertThat(licenseState.isAuthAllowed(), is(true)); - assertThat(licenseState.isIpFilteringAllowed(), is(true)); - assertThat(licenseState.isAuditingAllowed(), is(true)); + assertThat(licenseState.isAuthAllowed(), is(false)); + assertThat(licenseState.isIpFilteringAllowed(), is(false)); + assertThat(licenseState.isAuditingAllowed(), is(false)); assertThat(licenseState.isStatsAndHealthAllowed(), is(true)); - assertThat(licenseState.isDocumentAndFieldLevelSecurityAllowed(), is(true)); - assertThat(licenseState.allowedRealmType(), is(XPackLicenseState.AllowedRealmType.ALL)); - assertThat(licenseState.isCustomRoleProvidersAllowed(), is(true)); + assertThat(licenseState.isDocumentAndFieldLevelSecurityAllowed(), is(false)); + assertThat(licenseState.allowedRealmType(), is(XPackLicenseState.AllowedRealmType.NONE)); + assertThat(licenseState.isCustomRoleProvidersAllowed(), is(false)); } public void testSecurityBasic() { @@ -217,21 +217,21 @@ public class XPackLicenseStateTests extends ESTestCase { XPackLicenseState licenseState = new XPackLicenseState(Settings.EMPTY); licenseState.update(TRIAL, true, VersionUtils.randomVersionBetween(random(), Version.V_6_3_0, Version.CURRENT)); - assertThat(licenseState.isSecurityEnabled(), is(false)); - assertThat(licenseState.isAuthAllowed(), is(true)); - assertThat(licenseState.isIpFilteringAllowed(), is(true)); - assertThat(licenseState.isAuditingAllowed(), is(true)); + assertThat(licenseState.isSecurityDisabledByTrialLicense(), is(true)); + assertThat(licenseState.isAuthAllowed(), is(false)); + assertThat(licenseState.isIpFilteringAllowed(), is(false)); + assertThat(licenseState.isAuditingAllowed(), is(false)); assertThat(licenseState.isStatsAndHealthAllowed(), is(true)); - assertThat(licenseState.isDocumentAndFieldLevelSecurityAllowed(), is(true)); - assertThat(licenseState.allowedRealmType(), is(XPackLicenseState.AllowedRealmType.ALL)); - assertThat(licenseState.isCustomRoleProvidersAllowed(), is(true)); + assertThat(licenseState.isDocumentAndFieldLevelSecurityAllowed(), is(false)); + assertThat(licenseState.allowedRealmType(), is(XPackLicenseState.AllowedRealmType.NONE)); + assertThat(licenseState.isCustomRoleProvidersAllowed(), is(false)); } public void testOldTrialDefaultsSecurityOn() { XPackLicenseState licenseState = new XPackLicenseState(Settings.EMPTY); licenseState.update(TRIAL, true, rarely() ? null : VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.V_6_2_4)); - assertThat(licenseState.isSecurityEnabled(), is(true)); + assertThat(licenseState.isSecurityDisabledByTrialLicense(), is(false)); assertThat(licenseState.isAuthAllowed(), is(true)); assertThat(licenseState.isIpFilteringAllowed(), is(true)); assertThat(licenseState.isAuditingAllowed(), is(true)); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexSearcherWrapperIntegrationTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexSearcherWrapperIntegrationTests.java index 9abaaf0ecf0..ac6e0d0e151 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexSearcherWrapperIntegrationTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexSearcherWrapperIntegrationTests.java @@ -86,7 +86,6 @@ public class SecurityIndexSearcherWrapperIntegrationTests extends ESTestCase { }); XPackLicenseState licenseState = mock(XPackLicenseState.class); when(licenseState.isDocumentAndFieldLevelSecurityAllowed()).thenReturn(true); - when(licenseState.isSecurityEnabled()).thenReturn(true); SecurityIndexSearcherWrapper wrapper = new SecurityIndexSearcherWrapper(indexSettings, s -> queryShardContext, bitsetFilterCache, threadContext, licenseState, scriptService) { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexSearcherWrapperUnitTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexSearcherWrapperUnitTests.java index e364b0a7e8a..207c9d22198 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexSearcherWrapperUnitTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexSearcherWrapperUnitTests.java @@ -131,7 +131,6 @@ public class SecurityIndexSearcherWrapperUnitTests extends ESTestCase { ShardId shardId = new ShardId(index, 0); licenseState = mock(XPackLicenseState.class); - when(licenseState.isSecurityEnabled()).thenReturn(true); when(licenseState.isDocumentAndFieldLevelSecurityAllowed()).thenReturn(true); threadContext = new ThreadContext(Settings.EMPTY); IndexShard indexShard = mock(IndexShard.class); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportPutDatafeedAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportPutDatafeedAction.java index 7a7deac0136..60b8235ec84 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportPutDatafeedAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportPutDatafeedAction.java @@ -77,7 +77,7 @@ public class TransportPutDatafeedAction extends TransportMasterNodeAction listener) { // If security is enabled only create the datafeed if the user requesting creation has // permission to read the indices the datafeed is going to read from - if (licenseState.isSecurityEnabled() && licenseState.isAuthAllowed()) { + if (licenseState.isAuthAllowed()) { final String username = securityContext.getUser().principal(); ActionListener privResponseListener = ActionListener.wrap( r -> handlePrivsResponse(username, request, r, listener), diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 363cc7bb882..42a2ad767d3 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -971,7 +971,7 @@ public class Security extends Plugin implements ActionPlugin, IngestPlugin, Netw public Function> getFieldFilter() { if (enabled) { return index -> { - if (getLicenseState().isSecurityEnabled() == false || getLicenseState().isDocumentAndFieldLevelSecurityAllowed() == false) { + if (getLicenseState().isDocumentAndFieldLevelSecurityAllowed() == false) { return MapperPlugin.NOOP_FIELD_PREDICATE; } IndicesAccessControl indicesAccessControl = threadContext.get().getTransient( diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/SecurityFeatureSet.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/SecurityFeatureSet.java index ab70b8513de..6f357790d2f 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/SecurityFeatureSet.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/SecurityFeatureSet.java @@ -76,7 +76,11 @@ public class SecurityFeatureSet implements XPackFeatureSet { @Override public boolean enabled() { - return licenseState != null && licenseState.isSecurityEnabled(); + if (licenseState != null) { + return XPackSettings.SECURITY_ENABLED.get(settings) && + licenseState.isSecurityDisabledByTrialLicense() == false; + } + return false; } @Override diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilter.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilter.java index 353b4b9729b..3e1f9f97c2f 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilter.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilter.java @@ -72,7 +72,6 @@ public class SecurityActionFilter extends AbstractComponent implements ActionFil public void apply(Task task, String action, Request request, ActionListener listener, ActionFilterChain chain) { - /* A functional requirement - when the license of security is disabled (invalid/expires), security will continue to operate normally, except all read operations will be blocked. @@ -84,8 +83,7 @@ public class SecurityActionFilter extends AbstractComponent implements ActionFil throw LicenseUtils.newComplianceException(XPackField.SECURITY); } - final boolean securityEnabled = licenseState.isSecurityEnabled(); - if (securityEnabled && licenseState.isAuthAllowed()) { + if (licenseState.isAuthAllowed()) { final ActionListener contextPreservingListener = ContextPreservingActionListener.wrapPreservingContext(listener, threadContext); ActionListener authenticatedListener = ActionListener.wrap( @@ -117,7 +115,7 @@ public class SecurityActionFilter extends AbstractComponent implements ActionFil listener.onFailure(e); } } else if (SECURITY_ACTION_MATCHER.test(action)) { - if (securityEnabled == false && licenseState.isTrialLicense()) { + if (licenseState.isSecurityDisabledByTrialLicense()) { listener.onFailure(new ElasticsearchException("Security must be explicitly enabled when using a trial license. " + "Enable security by setting [xpack.security.enabled] to [true] in the elasticsearch.yml file " + "and restart the node.")); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/BulkShardRequestInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/BulkShardRequestInterceptor.java index cbcdce98eaa..abdaba7cf29 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/BulkShardRequestInterceptor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/BulkShardRequestInterceptor.java @@ -37,25 +37,25 @@ public class BulkShardRequestInterceptor extends AbstractComponent implements Re @Override public void intercept(BulkShardRequest request, Authentication authentication, Role userPermissions, String action) { - if (licenseState.isSecurityEnabled() == false || licenseState.isDocumentAndFieldLevelSecurityAllowed() == false) { - return; - } - IndicesAccessControl indicesAccessControl = threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY); + if (licenseState.isDocumentAndFieldLevelSecurityAllowed()) { + IndicesAccessControl indicesAccessControl = threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY); - for (BulkItemRequest bulkItemRequest : request.items()) { - IndicesAccessControl.IndexAccessControl indexAccessControl = indicesAccessControl.getIndexPermissions(bulkItemRequest.index()); - if (indexAccessControl != null) { - boolean fls = indexAccessControl.getFieldPermissions().hasFieldLevelSecurity(); - boolean dls = indexAccessControl.getQueries() != null; - if (fls || dls) { - if (bulkItemRequest.request() instanceof UpdateRequest) { - throw new ElasticsearchSecurityException("Can't execute a bulk request with update requests embedded if " + + for (BulkItemRequest bulkItemRequest : request.items()) { + IndicesAccessControl.IndexAccessControl indexAccessControl = + indicesAccessControl.getIndexPermissions(bulkItemRequest.index()); + if (indexAccessControl != null) { + boolean fls = indexAccessControl.getFieldPermissions().hasFieldLevelSecurity(); + boolean dls = indexAccessControl.getQueries() != null; + if (fls || dls) { + if (bulkItemRequest.request() instanceof UpdateRequest) { + throw new ElasticsearchSecurityException("Can't execute a bulk request with update requests embedded if " + "field or document level security is enabled", RestStatus.BAD_REQUEST); + } } } - } - logger.trace("intercepted bulk request for index [{}] without any update requests, continuing execution", + logger.trace("intercepted bulk request for index [{}] without any update requests, continuing execution", bulkItemRequest.index()); + } } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/FieldAndDocumentLevelSecurityRequestInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/FieldAndDocumentLevelSecurityRequestInterceptor.java index 5116e9b09f8..5f6f4d1643b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/FieldAndDocumentLevelSecurityRequestInterceptor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/FieldAndDocumentLevelSecurityRequestInterceptor.java @@ -34,26 +34,25 @@ abstract class FieldAndDocumentLevelSecurityRequestInterceptor permissionsMap = new HashMap<>(); - for (IndicesAliasesRequest.AliasActions aliasAction : request.getAliasActions()) { - if (aliasAction.actionType() == IndicesAliasesRequest.AliasActions.Type.ADD) { - for (String index : aliasAction.indices()) { - Automaton indexPermissions = permissionsMap.computeIfAbsent(index, userPermissions.indices()::allowedActionsMatcher); - for (String alias : aliasAction.aliases()) { - Automaton aliasPermissions = + Map permissionsMap = new HashMap<>(); + for (IndicesAliasesRequest.AliasActions aliasAction : request.getAliasActions()) { + if (aliasAction.actionType() == IndicesAliasesRequest.AliasActions.Type.ADD) { + for (String index : aliasAction.indices()) { + Automaton indexPermissions = + permissionsMap.computeIfAbsent(index, userPermissions.indices()::allowedActionsMatcher); + for (String alias : aliasAction.aliases()) { + Automaton aliasPermissions = permissionsMap.computeIfAbsent(alias, userPermissions.indices()::allowedActionsMatcher); - if (Operations.subsetOf(aliasPermissions, indexPermissions) == false) { - // TODO we've already audited a access granted event so this is going to look ugly - auditTrailService.accessDenied(authentication, action, request, userPermissions.names()); - throw Exceptions.authorizationError("Adding an alias is not allowed when the alias " + + if (Operations.subsetOf(aliasPermissions, indexPermissions) == false) { + // TODO we've already audited a access granted event so this is going to look ugly + auditTrailService.accessDenied(authentication, action, request, userPermissions.names()); + throw Exceptions.authorizationError("Adding an alias is not allowed when the alias " + "has more permissions than any of the indices"); + } } } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/ResizeRequestInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/ResizeRequestInterceptor.java index a4d5eecb92f..255f46cb02c 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/ResizeRequestInterceptor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/ResizeRequestInterceptor.java @@ -39,31 +39,33 @@ public final class ResizeRequestInterceptor extends AbstractComponent implements @Override public void intercept(ResizeRequest request, Authentication authentication, Role userPermissions, String action) { - if (licenseState.isSecurityEnabled() == false) { - return; - } - - if (licenseState.isDocumentAndFieldLevelSecurityAllowed()) { - IndicesAccessControl indicesAccessControl = threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY); - IndicesAccessControl.IndexAccessControl indexAccessControl = indicesAccessControl.getIndexPermissions(request.getSourceIndex()); - if (indexAccessControl != null) { - final boolean fls = indexAccessControl.getFieldPermissions().hasFieldLevelSecurity(); - final boolean dls = indexAccessControl.getQueries() != null; - if (fls || dls) { - throw new ElasticsearchSecurityException("Resize requests are not allowed for users when " + + final XPackLicenseState frozenLicenseState = licenseState.copyCurrentLicenseState(); + if (frozenLicenseState.isAuthAllowed()) { + if (frozenLicenseState.isDocumentAndFieldLevelSecurityAllowed()) { + IndicesAccessControl indicesAccessControl = + threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY); + IndicesAccessControl.IndexAccessControl indexAccessControl = + indicesAccessControl.getIndexPermissions(request.getSourceIndex()); + if (indexAccessControl != null) { + final boolean fls = indexAccessControl.getFieldPermissions().hasFieldLevelSecurity(); + final boolean dls = indexAccessControl.getQueries() != null; + if (fls || dls) { + throw new ElasticsearchSecurityException("Resize requests are not allowed for users when " + "field or document level security is enabled on the source index", RestStatus.BAD_REQUEST); + } } } - } - // ensure that the user would have the same level of access OR less on the target index - final Automaton sourceIndexPermissions = userPermissions.indices().allowedActionsMatcher(request.getSourceIndex()); - final Automaton targetIndexPermissions = userPermissions.indices().allowedActionsMatcher(request.getTargetIndexRequest().index()); - if (Operations.subsetOf(targetIndexPermissions, sourceIndexPermissions) == false) { - // TODO we've already audited a access granted event so this is going to look ugly - auditTrailService.accessDenied(authentication, action, request, userPermissions.names()); - throw Exceptions.authorizationError("Resizing an index is not allowed when the target index " + + // ensure that the user would have the same level of access OR less on the target index + final Automaton sourceIndexPermissions = userPermissions.indices().allowedActionsMatcher(request.getSourceIndex()); + final Automaton targetIndexPermissions = + userPermissions.indices().allowedActionsMatcher(request.getTargetIndexRequest().index()); + if (Operations.subsetOf(targetIndexPermissions, sourceIndexPermissions) == false) { + // TODO we've already audited a access granted event so this is going to look ugly + auditTrailService.accessDenied(authentication, action, request, userPermissions.names()); + throw Exceptions.authorizationError("Resizing an index is not allowed when the target index " + "has more permissions than the source index"); + } } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrailService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrailService.java index 3cd12b1a7ce..e36dee3d67c 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrailService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrailService.java @@ -42,7 +42,7 @@ public class AuditTrailService extends AbstractComponent implements AuditTrail { @Override public void authenticationSuccess(String realm, User user, RestRequest request) { - if (licenseState.isSecurityEnabled() && licenseState.isAuditingAllowed()) { + if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { auditTrail.authenticationSuccess(realm, user, request); } @@ -51,7 +51,7 @@ public class AuditTrailService extends AbstractComponent implements AuditTrail { @Override public void authenticationSuccess(String realm, User user, String action, TransportMessage message) { - if (licenseState.isSecurityEnabled() && licenseState.isAuditingAllowed()) { + if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { auditTrail.authenticationSuccess(realm, user, action, message); } @@ -60,7 +60,7 @@ public class AuditTrailService extends AbstractComponent implements AuditTrail { @Override public void anonymousAccessDenied(String action, TransportMessage message) { - if (licenseState.isSecurityEnabled() && licenseState.isAuditingAllowed()) { + if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { auditTrail.anonymousAccessDenied(action, message); } @@ -69,7 +69,7 @@ public class AuditTrailService extends AbstractComponent implements AuditTrail { @Override public void anonymousAccessDenied(RestRequest request) { - if (licenseState.isSecurityEnabled() && licenseState.isAuditingAllowed()) { + if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { auditTrail.anonymousAccessDenied(request); } @@ -78,7 +78,7 @@ public class AuditTrailService extends AbstractComponent implements AuditTrail { @Override public void authenticationFailed(RestRequest request) { - if (licenseState.isSecurityEnabled() && licenseState.isAuditingAllowed()) { + if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { auditTrail.authenticationFailed(request); } @@ -87,7 +87,7 @@ public class AuditTrailService extends AbstractComponent implements AuditTrail { @Override public void authenticationFailed(String action, TransportMessage message) { - if (licenseState.isSecurityEnabled() && licenseState.isAuditingAllowed()) { + if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { auditTrail.authenticationFailed(action, message); } @@ -96,7 +96,7 @@ public class AuditTrailService extends AbstractComponent implements AuditTrail { @Override public void authenticationFailed(AuthenticationToken token, String action, TransportMessage message) { - if (licenseState.isSecurityEnabled() && licenseState.isAuditingAllowed()) { + if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { auditTrail.authenticationFailed(token, action, message); } @@ -105,7 +105,7 @@ public class AuditTrailService extends AbstractComponent implements AuditTrail { @Override public void authenticationFailed(String realm, AuthenticationToken token, String action, TransportMessage message) { - if (licenseState.isSecurityEnabled() && licenseState.isAuditingAllowed()) { + if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { auditTrail.authenticationFailed(realm, token, action, message); } @@ -114,7 +114,7 @@ public class AuditTrailService extends AbstractComponent implements AuditTrail { @Override public void authenticationFailed(AuthenticationToken token, RestRequest request) { - if (licenseState.isSecurityEnabled() && licenseState.isAuditingAllowed()) { + if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { auditTrail.authenticationFailed(token, request); } @@ -123,7 +123,7 @@ public class AuditTrailService extends AbstractComponent implements AuditTrail { @Override public void authenticationFailed(String realm, AuthenticationToken token, RestRequest request) { - if (licenseState.isSecurityEnabled() && licenseState.isAuditingAllowed()) { + if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { auditTrail.authenticationFailed(realm, token, request); } @@ -132,7 +132,7 @@ public class AuditTrailService extends AbstractComponent implements AuditTrail { @Override public void accessGranted(Authentication authentication, String action, TransportMessage message, String[] roleNames) { - if (licenseState.isSecurityEnabled() && licenseState.isAuditingAllowed()) { + if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { auditTrail.accessGranted(authentication, action, message, roleNames); } @@ -141,7 +141,7 @@ public class AuditTrailService extends AbstractComponent implements AuditTrail { @Override public void accessDenied(Authentication authentication, String action, TransportMessage message, String[] roleNames) { - if (licenseState.isSecurityEnabled() && licenseState.isAuditingAllowed()) { + if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { auditTrail.accessDenied(authentication, action, message, roleNames); } @@ -150,7 +150,7 @@ public class AuditTrailService extends AbstractComponent implements AuditTrail { @Override public void tamperedRequest(RestRequest request) { - if (licenseState.isSecurityEnabled() && licenseState.isAuditingAllowed()) { + if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { auditTrail.tamperedRequest(request); } @@ -159,7 +159,7 @@ public class AuditTrailService extends AbstractComponent implements AuditTrail { @Override public void tamperedRequest(String action, TransportMessage message) { - if (licenseState.isSecurityEnabled() && licenseState.isAuditingAllowed()) { + if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { auditTrail.tamperedRequest(action, message); } @@ -168,7 +168,7 @@ public class AuditTrailService extends AbstractComponent implements AuditTrail { @Override public void tamperedRequest(User user, String action, TransportMessage request) { - if (licenseState.isSecurityEnabled() && licenseState.isAuditingAllowed()) { + if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { auditTrail.tamperedRequest(user, action, request); } @@ -177,7 +177,7 @@ public class AuditTrailService extends AbstractComponent implements AuditTrail { @Override public void connectionGranted(InetAddress inetAddress, String profile, SecurityIpFilterRule rule) { - if (licenseState.isSecurityEnabled() && licenseState.isAuditingAllowed()) { + if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { auditTrail.connectionGranted(inetAddress, profile, rule); } @@ -186,7 +186,7 @@ public class AuditTrailService extends AbstractComponent implements AuditTrail { @Override public void connectionDenied(InetAddress inetAddress, String profile, SecurityIpFilterRule rule) { - if (licenseState.isSecurityEnabled() && licenseState.isAuditingAllowed()) { + if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { auditTrail.connectionDenied(inetAddress, profile, rule); } @@ -195,7 +195,7 @@ public class AuditTrailService extends AbstractComponent implements AuditTrail { @Override public void runAsGranted(Authentication authentication, String action, TransportMessage message, String[] roleNames) { - if (licenseState.isSecurityEnabled() && licenseState.isAuditingAllowed()) { + if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { auditTrail.runAsGranted(authentication, action, message, roleNames); } @@ -204,7 +204,7 @@ public class AuditTrailService extends AbstractComponent implements AuditTrail { @Override public void runAsDenied(Authentication authentication, String action, TransportMessage message, String[] roleNames) { - if (licenseState.isSecurityEnabled() && licenseState.isAuditingAllowed()) { + if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { auditTrail.runAsDenied(authentication, action, message, roleNames); } @@ -213,7 +213,7 @@ public class AuditTrailService extends AbstractComponent implements AuditTrail { @Override public void runAsDenied(Authentication authentication, RestRequest request, String[] roleNames) { - if (licenseState.isSecurityEnabled() && licenseState.isAuditingAllowed()) { + if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { auditTrail.runAsDenied(authentication, request, roleNames); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/Realms.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/Realms.java index d2573b9343d..ce45ee2bedf 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/Realms.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/Realms.java @@ -98,7 +98,7 @@ public class Realms extends AbstractComponent implements Iterable { @Override public Iterator iterator() { - if (licenseState.isSecurityEnabled() == false || licenseState.isAuthAllowed() == false) { + if (licenseState.isAuthAllowed() == false) { return Collections.emptyIterator(); } @@ -120,7 +120,7 @@ public class Realms extends AbstractComponent implements Iterable { } public List asList() { - if (licenseState.isSecurityEnabled() == false || licenseState.isAuthAllowed() == false) { + if (licenseState.isAuthAllowed() == false) { return Collections.emptyList(); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListener.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListener.java index 6658d095b9c..e3121c9512d 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListener.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListener.java @@ -45,7 +45,7 @@ public final class SecuritySearchOperationListener implements SearchOperationLis */ @Override public void onNewScrollContext(SearchContext searchContext) { - if (licenseState.isSecurityEnabled() && licenseState.isAuthAllowed()) { + if (licenseState.isAuthAllowed()) { searchContext.scrollContext().putInContext(AuthenticationField.AUTHENTICATION_KEY, Authentication.getAuthentication(threadContext)); } @@ -57,7 +57,7 @@ public final class SecuritySearchOperationListener implements SearchOperationLis */ @Override public void validateSearchContext(SearchContext searchContext, TransportRequest request) { - if (licenseState.isSecurityEnabled() && licenseState.isAuthAllowed()) { + if (licenseState.isAuthAllowed()) { if (searchContext.scrollContext() != null) { final Authentication originalAuth = searchContext.scrollContext().getFromContext(AuthenticationField.AUTHENTICATION_KEY); final Authentication current = Authentication.getAuthentication(threadContext); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/accesscontrol/OptOutQueryCache.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/accesscontrol/OptOutQueryCache.java index a49bfdfbe16..1ace72a1da0 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/accesscontrol/OptOutQueryCache.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/accesscontrol/OptOutQueryCache.java @@ -59,8 +59,7 @@ public final class OptOutQueryCache extends AbstractIndexComponent implements Qu @Override public Weight doCache(Weight weight, QueryCachingPolicy policy) { - // TODO: this is not concurrently safe since the license state can change between reads - if (licenseState.isSecurityEnabled() == false || licenseState.isAuthAllowed() == false) { + if (licenseState.isAuthAllowed() == false) { logger.debug("not opting out of the query cache; authorization is not allowed"); return indicesQueryCache.doCache(weight, policy); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/SecurityRestFilter.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/SecurityRestFilter.java index 8d304302e03..7b14f218c43 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/SecurityRestFilter.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/SecurityRestFilter.java @@ -46,7 +46,7 @@ public class SecurityRestFilter implements RestHandler { @Override public void handleRequest(RestRequest request, RestChannel channel, NodeClient client) throws Exception { - if (licenseState.isSecurityEnabled() && licenseState.isAuthAllowed() && request.method() != Method.OPTIONS) { + if (licenseState.isAuthAllowed() && request.method() != Method.OPTIONS) { // CORS - allow for preflight unauthenticated OPTIONS request if (extractClientCertificate) { HttpChannel httpChannel = request.getHttpChannel(); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/SecurityBaseRestHandler.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/SecurityBaseRestHandler.java index 9006ec620b5..dd1e387b989 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/SecurityBaseRestHandler.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/SecurityBaseRestHandler.java @@ -14,6 +14,7 @@ import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.BytesRestResponse; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.xpack.core.XPackField; +import org.elasticsearch.xpack.core.XPackSettings; import java.io.IOException; @@ -64,16 +65,14 @@ public abstract class SecurityBaseRestHandler extends BaseRestHandler { * sent to the requestor */ protected Exception checkFeatureAvailable(RestRequest request) { - if (licenseState.isSecurityAvailable() == false) { + if (XPackSettings.SECURITY_ENABLED.get(settings) == false) { + return new IllegalStateException("Security is not enabled but a security rest handler is registered"); + } else if (licenseState.isSecurityAvailable() == false) { return LicenseUtils.newComplianceException(XPackField.SECURITY); - } else if (licenseState.isSecurityEnabled() == false) { - if (licenseState.isTrialLicense()) { - return new ElasticsearchException("Security must be explicitly enabled when using a trial license. " + - "Enable security by setting [xpack.security.enabled] to [true] in the elasticsearch.yml file " + - "and restart the node."); - } else { - return new IllegalStateException("Security is not enabled but a security rest handler is registered"); - } + } else if (licenseState.isSecurityDisabledByTrialLicense()) { + return new ElasticsearchException("Security must be explicitly enabled when using a trial license. " + + "Enable security by setting [xpack.security.enabled] to [true] in the elasticsearch.yml file " + + "and restart the node."); } else { return null; } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java index 3b761522fa7..14081e136d3 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java @@ -107,7 +107,7 @@ public class SecurityServerTransportInterceptor extends AbstractComponent implem // guarantee we use the same value wherever we would check the value for the state // being recovered final boolean stateNotRecovered = isStateNotRecovered; - final boolean sendWithAuth = (licenseState.isSecurityEnabled() && licenseState.isAuthAllowed()) || stateNotRecovered; + final boolean sendWithAuth = licenseState.isAuthAllowed() || stateNotRecovered; if (sendWithAuth) { // the transport in core normally does this check, BUT since we are serializing to a string header we need to do it // ourselves otherwise we wind up using a version newer than what we can actually send @@ -266,7 +266,7 @@ public class SecurityServerTransportInterceptor extends AbstractComponent implem public void messageReceived(T request, TransportChannel channel, Task task) throws Exception { final AbstractRunnable receiveMessage = getReceiveRunnable(request, channel, task); try (ThreadContext.StoredContext ctx = threadContext.newStoredContext(true)) { - if (licenseState.isSecurityEnabled() && licenseState.isAuthAllowed()) { + if (licenseState.isAuthAllowed()) { String profile = channel.getProfileName(); ServerTransportFilter filter = profileFilters.get(profile); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/filter/IPFilter.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/filter/IPFilter.java index 586e9cd6507..860d6bb69b6 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/filter/IPFilter.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/filter/IPFilter.java @@ -198,7 +198,7 @@ public class IPFilter { } public boolean accept(String profile, InetSocketAddress peerAddress) { - if (licenseState.isSecurityEnabled() == false || licenseState.isIpFilteringAllowed() == false) { + if (licenseState.isIpFilteringAllowed() == false) { return true; } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/license/LicensingTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/license/LicensingTests.java index 7a35b0bc422..a63dd94b639 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/license/LicensingTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/license/LicensingTests.java @@ -238,7 +238,7 @@ public class LicensingTests extends SecurityIntegTestCase { License.OperationMode mode = randomFrom(License.OperationMode.GOLD, License.OperationMode.TRIAL, License.OperationMode.PLATINUM, License.OperationMode.STANDARD); enableLicensing(mode); - // security actions should not work! + // security actions should work! try (TransportClient client = new TestXPackTransportClient(settings, LocalStateSecurity.class)) { client.addTransportAddress(internalCluster().getDataNodeInstance(Transport.class).boundAddress().publishAddress()); GetUsersResponse response = new SecurityClient(client).prepareGetUsers().get(); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityFeatureSetTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityFeatureSetTests.java index 076ce6c9fcb..2944cd3134a 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityFeatureSetTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityFeatureSetTests.java @@ -55,7 +55,6 @@ public class SecurityFeatureSetTests extends ESTestCase { public void init() throws Exception { settings = Settings.builder().put("path.home", createTempDir()).build(); licenseState = mock(XPackLicenseState.class); - when(licenseState.isSecurityEnabled()).thenReturn(true); realms = mock(Realms.class); ipFilter = mock(IPFilter.class); rolesStore = mock(CompositeRolesStore.class); @@ -77,7 +76,7 @@ public class SecurityFeatureSetTests extends ESTestCase { rolesStore, roleMappingStore, ipFilter); assertThat(featureSet.enabled(), is(true)); - when(licenseState.isSecurityEnabled()).thenReturn(false); + when(licenseState.isSecurityDisabledByTrialLicense()).thenReturn(true); featureSet = new SecurityFeatureSet(settings, licenseState, realms, rolesStore, roleMappingStore, ipFilter); assertThat(featureSet.enabled(), is(false)); @@ -90,7 +89,7 @@ public class SecurityFeatureSetTests extends ESTestCase { Settings.Builder settings = Settings.builder().put(this.settings); boolean enabled = randomBoolean(); - when(licenseState.isSecurityEnabled()).thenReturn(enabled); + settings.put(XPackSettings.SECURITY_ENABLED.getKey(), enabled); final boolean httpSSLEnabled = randomBoolean(); settings.put("xpack.security.http.ssl.enabled", httpSSLEnabled); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilterTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilterTests.java index 577c7ddb249..93df605a74f 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilterTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/filter/SecurityActionFilterTests.java @@ -67,7 +67,6 @@ public class SecurityActionFilterTests extends ESTestCase { licenseState = mock(XPackLicenseState.class); when(licenseState.isAuthAllowed()).thenReturn(true); when(licenseState.isStatsAndHealthAllowed()).thenReturn(true); - when(licenseState.isSecurityEnabled()).thenReturn(true); ThreadPool threadPool = mock(ThreadPool.class); threadContext = new ThreadContext(Settings.EMPTY); when(threadPool.getThreadContext()).thenReturn(threadContext); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/interceptor/IndicesAliasesRequestInterceptorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/interceptor/IndicesAliasesRequestInterceptorTests.java index 7c951c0014e..a5798be9746 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/interceptor/IndicesAliasesRequestInterceptorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/interceptor/IndicesAliasesRequestInterceptorTests.java @@ -35,7 +35,8 @@ public class IndicesAliasesRequestInterceptorTests extends ESTestCase { public void testInterceptorThrowsWhenFLSDLSEnabled() { XPackLicenseState licenseState = mock(XPackLicenseState.class); - when(licenseState.isSecurityEnabled()).thenReturn(true); + when(licenseState.copyCurrentLicenseState()).thenReturn(licenseState); + when(licenseState.isAuthAllowed()).thenReturn(true); when(licenseState.isAuditingAllowed()).thenReturn(true); when(licenseState.isDocumentAndFieldLevelSecurityAllowed()).thenReturn(true); ThreadContext threadContext = new ThreadContext(Settings.EMPTY); @@ -81,7 +82,8 @@ public class IndicesAliasesRequestInterceptorTests extends ESTestCase { public void testInterceptorThrowsWhenTargetHasGreaterPermissions() throws Exception { XPackLicenseState licenseState = mock(XPackLicenseState.class); - when(licenseState.isSecurityEnabled()).thenReturn(true); + when(licenseState.copyCurrentLicenseState()).thenReturn(licenseState); + when(licenseState.isAuthAllowed()).thenReturn(true); when(licenseState.isAuditingAllowed()).thenReturn(true); when(licenseState.isDocumentAndFieldLevelSecurityAllowed()).thenReturn(true); ThreadContext threadContext = new ThreadContext(Settings.EMPTY); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/interceptor/ResizeRequestInterceptorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/interceptor/ResizeRequestInterceptorTests.java index f1363214b07..008928794db 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/interceptor/ResizeRequestInterceptorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/interceptor/ResizeRequestInterceptorTests.java @@ -37,7 +37,8 @@ public class ResizeRequestInterceptorTests extends ESTestCase { public void testResizeRequestInterceptorThrowsWhenFLSDLSEnabled() { XPackLicenseState licenseState = mock(XPackLicenseState.class); - when(licenseState.isSecurityEnabled()).thenReturn(true); + when(licenseState.copyCurrentLicenseState()).thenReturn(licenseState); + when(licenseState.isAuthAllowed()).thenReturn(true); when(licenseState.isAuditingAllowed()).thenReturn(true); when(licenseState.isDocumentAndFieldLevelSecurityAllowed()).thenReturn(true); ThreadPool threadPool = mock(ThreadPool.class); @@ -76,7 +77,8 @@ public class ResizeRequestInterceptorTests extends ESTestCase { public void testResizeRequestInterceptorThrowsWhenTargetHasGreaterPermissions() throws Exception { XPackLicenseState licenseState = mock(XPackLicenseState.class); - when(licenseState.isSecurityEnabled()).thenReturn(true); + when(licenseState.copyCurrentLicenseState()).thenReturn(licenseState); + when(licenseState.isAuthAllowed()).thenReturn(true); when(licenseState.isAuditingAllowed()).thenReturn(true); when(licenseState.isDocumentAndFieldLevelSecurityAllowed()).thenReturn(true); ThreadPool threadPool = mock(ThreadPool.class); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/AuditTrailServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/AuditTrailServiceTests.java index b346fc6857e..13a7e5c3cf7 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/AuditTrailServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/AuditTrailServiceTests.java @@ -48,7 +48,6 @@ public class AuditTrailServiceTests extends ESTestCase { licenseState = mock(XPackLicenseState.class); service = new AuditTrailService(Settings.EMPTY, auditTrails, licenseState); isAuditingAllowed = randomBoolean(); - when(licenseState.isSecurityEnabled()).thenReturn(true); when(licenseState.isAuditingAllowed()).thenReturn(isAuditingAllowed); token = mock(AuthenticationToken.class); message = mock(TransportMessage.class); @@ -58,7 +57,6 @@ public class AuditTrailServiceTests extends ESTestCase { public void testAuthenticationFailed() throws Exception { service.authenticationFailed(token, "_action", message); verify(licenseState).isAuditingAllowed(); - verify(licenseState).isSecurityEnabled(); if (isAuditingAllowed) { for (AuditTrail auditTrail : auditTrails) { verify(auditTrail).authenticationFailed(token, "_action", message); @@ -71,7 +69,6 @@ public class AuditTrailServiceTests extends ESTestCase { public void testAuthenticationFailedNoToken() throws Exception { service.authenticationFailed("_action", message); verify(licenseState).isAuditingAllowed(); - verify(licenseState).isSecurityEnabled(); if (isAuditingAllowed) { for (AuditTrail auditTrail : auditTrails) { verify(auditTrail).authenticationFailed("_action", message); @@ -84,7 +81,6 @@ public class AuditTrailServiceTests extends ESTestCase { public void testAuthenticationFailedRestNoToken() throws Exception { service.authenticationFailed(restRequest); verify(licenseState).isAuditingAllowed(); - verify(licenseState).isSecurityEnabled(); if (isAuditingAllowed) { for (AuditTrail auditTrail : auditTrails) { verify(auditTrail).authenticationFailed(restRequest); @@ -97,7 +93,6 @@ public class AuditTrailServiceTests extends ESTestCase { public void testAuthenticationFailedRest() throws Exception { service.authenticationFailed(token, restRequest); verify(licenseState).isAuditingAllowed(); - verify(licenseState).isSecurityEnabled(); if (isAuditingAllowed) { for (AuditTrail auditTrail : auditTrails) { verify(auditTrail).authenticationFailed(token, restRequest); @@ -110,7 +105,6 @@ public class AuditTrailServiceTests extends ESTestCase { public void testAuthenticationFailedRealm() throws Exception { service.authenticationFailed("_realm", token, "_action", message); verify(licenseState).isAuditingAllowed(); - verify(licenseState).isSecurityEnabled(); if (isAuditingAllowed) { for (AuditTrail auditTrail : auditTrails) { verify(auditTrail).authenticationFailed("_realm", token, "_action", message); @@ -123,7 +117,6 @@ public class AuditTrailServiceTests extends ESTestCase { public void testAuthenticationFailedRestRealm() throws Exception { service.authenticationFailed("_realm", token, restRequest); verify(licenseState).isAuditingAllowed(); - verify(licenseState).isSecurityEnabled(); if (isAuditingAllowed) { for (AuditTrail auditTrail : auditTrails) { verify(auditTrail).authenticationFailed("_realm", token, restRequest); @@ -136,7 +129,6 @@ public class AuditTrailServiceTests extends ESTestCase { public void testAnonymousAccess() throws Exception { service.anonymousAccessDenied("_action", message); verify(licenseState).isAuditingAllowed(); - verify(licenseState).isSecurityEnabled(); if (isAuditingAllowed) { for (AuditTrail auditTrail : auditTrails) { verify(auditTrail).anonymousAccessDenied("_action", message); @@ -152,7 +144,6 @@ public class AuditTrailServiceTests extends ESTestCase { String[] roles = new String[] { randomAlphaOfLengthBetween(1, 6) }; service.accessGranted(authentication, "_action", message, roles); verify(licenseState).isAuditingAllowed(); - verify(licenseState).isSecurityEnabled(); if (isAuditingAllowed) { for (AuditTrail auditTrail : auditTrails) { verify(auditTrail).accessGranted(authentication, "_action", message, roles); @@ -168,7 +159,6 @@ public class AuditTrailServiceTests extends ESTestCase { String[] roles = new String[] { randomAlphaOfLengthBetween(1, 6) }; service.accessDenied(authentication, "_action", message, roles); verify(licenseState).isAuditingAllowed(); - verify(licenseState).isSecurityEnabled(); if (isAuditingAllowed) { for (AuditTrail auditTrail : auditTrails) { verify(auditTrail).accessDenied(authentication, "_action", message, roles); @@ -183,7 +173,6 @@ public class AuditTrailServiceTests extends ESTestCase { SecurityIpFilterRule rule = randomBoolean() ? SecurityIpFilterRule.ACCEPT_ALL : IPFilter.DEFAULT_PROFILE_ACCEPT_ALL; service.connectionGranted(inetAddress, "client", rule); verify(licenseState).isAuditingAllowed(); - verify(licenseState).isSecurityEnabled(); if (isAuditingAllowed) { for (AuditTrail auditTrail : auditTrails) { verify(auditTrail).connectionGranted(inetAddress, "client", rule); @@ -198,7 +187,6 @@ public class AuditTrailServiceTests extends ESTestCase { SecurityIpFilterRule rule = new SecurityIpFilterRule(false, "_all"); service.connectionDenied(inetAddress, "client", rule); verify(licenseState).isAuditingAllowed(); - verify(licenseState).isSecurityEnabled(); if (isAuditingAllowed) { for (AuditTrail auditTrail : auditTrails) { verify(auditTrail).connectionDenied(inetAddress, "client", rule); @@ -213,7 +201,6 @@ public class AuditTrailServiceTests extends ESTestCase { String realm = "_realm"; service.authenticationSuccess(realm, user, restRequest); verify(licenseState).isAuditingAllowed(); - verify(licenseState).isSecurityEnabled(); if (isAuditingAllowed) { for (AuditTrail auditTrail : auditTrails) { verify(auditTrail).authenticationSuccess(realm, user, restRequest); @@ -228,7 +215,6 @@ public class AuditTrailServiceTests extends ESTestCase { String realm = "_realm"; service.authenticationSuccess(realm, user, "_action", message); verify(licenseState).isAuditingAllowed(); - verify(licenseState).isSecurityEnabled(); if (isAuditingAllowed) { for (AuditTrail auditTrail : auditTrails) { verify(auditTrail).authenticationSuccess(realm, user, "_action", message); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java index 1640ab727fe..65f69b397ba 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java @@ -154,7 +154,6 @@ public class AuthenticationServiceTests extends ESTestCase { XPackLicenseState licenseState = mock(XPackLicenseState.class); when(licenseState.allowedRealmType()).thenReturn(XPackLicenseState.AllowedRealmType.ALL); when(licenseState.isAuthAllowed()).thenReturn(true); - when(licenseState.isSecurityEnabled()).thenReturn(true); realms = new TestRealms(Settings.EMPTY, TestEnvironment.newEnvironment(settings), Collections.emptyMap(), licenseState, threadContext, mock(ReservedRealm.class), Arrays.asList(firstRealm, secondRealm), Collections.singletonList(firstRealm)); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/RealmsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/RealmsTests.java index 9d795826298..c5fbb39fee6 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/RealmsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/RealmsTests.java @@ -69,7 +69,6 @@ public class RealmsTests extends ESTestCase { threadContext = new ThreadContext(Settings.EMPTY); reservedRealm = mock(ReservedRealm.class); when(licenseState.isAuthAllowed()).thenReturn(true); - when(licenseState.isSecurityEnabled()).thenReturn(true); when(licenseState.allowedRealmType()).thenReturn(AllowedRealmType.ALL); when(reservedRealm.type()).thenReturn(ReservedRealm.TYPE); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListenerTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListenerTests.java index fac88e8af09..91d61e1ca5c 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListenerTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListenerTests.java @@ -39,7 +39,6 @@ public class SecuritySearchOperationListenerTests extends ESTestCase { public void testUnlicensed() { XPackLicenseState licenseState = mock(XPackLicenseState.class); - when(licenseState.isSecurityEnabled()).thenReturn(true); when(licenseState.isAuthAllowed()).thenReturn(false); ThreadContext threadContext = new ThreadContext(Settings.EMPTY); AuditTrailService auditTrailService = mock(AuditTrailService.class); @@ -49,7 +48,6 @@ public class SecuritySearchOperationListenerTests extends ESTestCase { SecuritySearchOperationListener listener = new SecuritySearchOperationListener(threadContext, licenseState, auditTrailService); listener.onNewScrollContext(searchContext); listener.validateSearchContext(searchContext, Empty.INSTANCE); - verify(licenseState, times(2)).isSecurityEnabled(); verify(licenseState, times(2)).isAuthAllowed(); verifyZeroInteractions(auditTrailService, searchContext); } @@ -60,7 +58,6 @@ public class SecuritySearchOperationListenerTests extends ESTestCase { final Scroll scroll = new Scroll(TimeValue.timeValueSeconds(2L)); testSearchContext.scrollContext().scroll = scroll; XPackLicenseState licenseState = mock(XPackLicenseState.class); - when(licenseState.isSecurityEnabled()).thenReturn(true); when(licenseState.isAuthAllowed()).thenReturn(true); ThreadContext threadContext = new ThreadContext(Settings.EMPTY); AuditTrailService auditTrailService = mock(AuditTrailService.class); @@ -75,7 +72,6 @@ public class SecuritySearchOperationListenerTests extends ESTestCase { assertEquals(scroll, testSearchContext.scrollContext().scroll); verify(licenseState).isAuthAllowed(); - verify(licenseState).isSecurityEnabled(); verifyZeroInteractions(auditTrailService); } @@ -86,7 +82,6 @@ public class SecuritySearchOperationListenerTests extends ESTestCase { new Authentication(new User("test", "role"), new RealmRef("realm", "file", "node"), null)); testSearchContext.scrollContext().scroll = new Scroll(TimeValue.timeValueSeconds(2L)); XPackLicenseState licenseState = mock(XPackLicenseState.class); - when(licenseState.isSecurityEnabled()).thenReturn(true); when(licenseState.isAuthAllowed()).thenReturn(true); ThreadContext threadContext = new ThreadContext(Settings.EMPTY); AuditTrailService auditTrailService = mock(AuditTrailService.class); @@ -97,7 +92,6 @@ public class SecuritySearchOperationListenerTests extends ESTestCase { authentication.writeToContext(threadContext); listener.validateSearchContext(testSearchContext, Empty.INSTANCE); verify(licenseState).isAuthAllowed(); - verify(licenseState).isSecurityEnabled(); verifyZeroInteractions(auditTrailService); } @@ -108,7 +102,6 @@ public class SecuritySearchOperationListenerTests extends ESTestCase { authentication.writeToContext(threadContext); listener.validateSearchContext(testSearchContext, Empty.INSTANCE); verify(licenseState, times(2)).isAuthAllowed(); - verify(licenseState, times(2)).isSecurityEnabled(); verifyZeroInteractions(auditTrailService); } @@ -125,7 +118,6 @@ public class SecuritySearchOperationListenerTests extends ESTestCase { expectThrows(SearchContextMissingException.class, () -> listener.validateSearchContext(testSearchContext, request)); assertEquals(testSearchContext.id(), expected.id()); verify(licenseState, times(3)).isAuthAllowed(); - verify(licenseState, times(3)).isSecurityEnabled(); verify(auditTrailService).accessDenied(authentication, "action", request, authentication.getUser().roles()); } @@ -142,7 +134,6 @@ public class SecuritySearchOperationListenerTests extends ESTestCase { final InternalScrollSearchRequest request = new InternalScrollSearchRequest(); listener.validateSearchContext(testSearchContext, request); verify(licenseState, times(4)).isAuthAllowed(); - verify(licenseState, times(4)).isSecurityEnabled(); verifyNoMoreInteractions(auditTrailService); } @@ -161,7 +152,6 @@ public class SecuritySearchOperationListenerTests extends ESTestCase { expectThrows(SearchContextMissingException.class, () -> listener.validateSearchContext(testSearchContext, request)); assertEquals(testSearchContext.id(), expected.id()); verify(licenseState, times(5)).isAuthAllowed(); - verify(licenseState, times(5)).isSecurityEnabled(); verify(auditTrailService).accessDenied(authentication, "action", request, authentication.getUser().roles()); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/OptOutQueryCacheTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/OptOutQueryCacheTests.java index efe154f8d78..d2b6c736fd8 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/OptOutQueryCacheTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/OptOutQueryCacheTests.java @@ -48,7 +48,7 @@ public class OptOutQueryCacheTests extends ESTestCase { DirectoryReader reader; @Before - void initLuceneStuff() throws IOException { + public void initLuceneStuff() throws IOException { dir = newDirectory(); w = new RandomIndexWriter(random(), dir); reader = w.getReader(); @@ -56,11 +56,12 @@ public class OptOutQueryCacheTests extends ESTestCase { } @After - void closeLuceneStuff() throws IOException { + public void closeLuceneStuff() throws IOException { w.close(); dir.close(); reader.close(); } + public void testOptOutQueryCacheSafetyCheck() throws IOException { BooleanQuery.Builder builder = new BooleanQuery.Builder(); @@ -123,25 +124,6 @@ public class OptOutQueryCacheTests extends ESTestCase { assertFalse(OptOutQueryCache.cachingIsSafe(weight, permissions)); } - public void testOptOutQueryCacheSecurityIsNotEnabled() { - final Settings.Builder settings = Settings.builder() - .put("index.version.created", Version.CURRENT) - .put("index.number_of_shards", 1) - .put("index.number_of_replicas", 0); - final IndexMetaData indexMetaData = IndexMetaData.builder("index").settings(settings).build(); - final IndexSettings indexSettings = new IndexSettings(indexMetaData, Settings.EMPTY); - final IndicesQueryCache indicesQueryCache = mock(IndicesQueryCache.class); - final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); - final XPackLicenseState licenseState = mock(XPackLicenseState.class); - when(licenseState.isSecurityEnabled()).thenReturn(false); - when(licenseState.isAuthAllowed()).thenReturn(randomBoolean()); - final OptOutQueryCache cache = new OptOutQueryCache(indexSettings, indicesQueryCache, threadContext, licenseState); - final Weight weight = mock(Weight.class); - final QueryCachingPolicy policy = mock(QueryCachingPolicy.class); - cache.doCache(weight, policy); - verify(indicesQueryCache).doCache(same(weight), same(policy)); - } - public void testOptOutQueryCacheAuthIsNotAllowed() { final Settings.Builder settings = Settings.builder() .put("index.version.created", Version.CURRENT) @@ -152,7 +134,6 @@ public class OptOutQueryCacheTests extends ESTestCase { final IndicesQueryCache indicesQueryCache = mock(IndicesQueryCache.class); final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); final XPackLicenseState licenseState = mock(XPackLicenseState.class); - when(licenseState.isSecurityEnabled()).thenReturn(randomBoolean()); when(licenseState.isAuthAllowed()).thenReturn(false); final OptOutQueryCache cache = new OptOutQueryCache(indexSettings, indicesQueryCache, threadContext, licenseState); final Weight weight = mock(Weight.class); @@ -171,7 +152,6 @@ public class OptOutQueryCacheTests extends ESTestCase { final IndicesQueryCache indicesQueryCache = mock(IndicesQueryCache.class); final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); final XPackLicenseState licenseState = mock(XPackLicenseState.class); - when(licenseState.isSecurityEnabled()).thenReturn(true); when(licenseState.isAuthAllowed()).thenReturn(true); final OptOutQueryCache cache = new OptOutQueryCache(indexSettings, indicesQueryCache, threadContext, licenseState); final Weight weight = mock(Weight.class); @@ -196,7 +176,6 @@ public class OptOutQueryCacheTests extends ESTestCase { when(indicesAccessControl.getIndexPermissions("index")).thenReturn(indexAccessControl); threadContext.putTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY, indicesAccessControl); final XPackLicenseState licenseState = mock(XPackLicenseState.class); - when(licenseState.isSecurityEnabled()).thenReturn(true); when(licenseState.isAuthAllowed()).thenReturn(true); final OptOutQueryCache cache = new OptOutQueryCache(indexSettings, indicesQueryCache, threadContext, licenseState); final Weight weight = mock(Weight.class); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/SecurityRestFilterTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/SecurityRestFilterTests.java index 5db634c8d7b..4c0ca977a21 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/SecurityRestFilterTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/SecurityRestFilterTests.java @@ -60,7 +60,6 @@ public class SecurityRestFilterTests extends ESTestCase { channel = mock(RestChannel.class); licenseState = mock(XPackLicenseState.class); when(licenseState.isAuthAllowed()).thenReturn(true); - when(licenseState.isSecurityEnabled()).thenReturn(true); restHandler = mock(RestHandler.class); filter = new SecurityRestFilter(licenseState, new ThreadContext(Settings.EMPTY), authcService, restHandler, false); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/SecurityBaseRestHandlerTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/SecurityBaseRestHandlerTests.java index c78d0a64745..4ff582f01bd 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/SecurityBaseRestHandlerTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/SecurityBaseRestHandlerTests.java @@ -24,11 +24,11 @@ import static org.mockito.Mockito.when; public class SecurityBaseRestHandlerTests extends ESTestCase { public void testSecurityBaseRestHandlerChecksLicenseState() throws Exception { - final boolean securityEnabled = randomBoolean(); + final boolean securityDisabledByTrial = randomBoolean(); final AtomicBoolean consumerCalled = new AtomicBoolean(false); final XPackLicenseState licenseState = mock(XPackLicenseState.class); when(licenseState.isSecurityAvailable()).thenReturn(true); - when(licenseState.isSecurityEnabled()).thenReturn(securityEnabled); + when(licenseState.isSecurityDisabledByTrialLicense()).thenReturn(securityDisabledByTrial); SecurityBaseRestHandler handler = new SecurityBaseRestHandler(Settings.EMPTY, licenseState) { @Override @@ -46,7 +46,7 @@ public class SecurityBaseRestHandlerTests extends ESTestCase { } }; FakeRestRequest fakeRestRequest = new FakeRestRequest(); - FakeRestChannel fakeRestChannel = new FakeRestChannel(fakeRestRequest, randomBoolean(), securityEnabled ? 0 : 1); + FakeRestChannel fakeRestChannel = new FakeRestChannel(fakeRestRequest, randomBoolean(), securityDisabledByTrial ? 1 : 0); NodeClient client = mock(NodeClient.class); assertFalse(consumerCalled.get()); @@ -54,8 +54,7 @@ public class SecurityBaseRestHandlerTests extends ESTestCase { handler.handleRequest(fakeRestRequest, fakeRestChannel, client); verify(licenseState).isSecurityAvailable(); - verify(licenseState).isSecurityEnabled(); - if (securityEnabled) { + if (securityDisabledByTrial == false) { assertTrue(consumerCalled.get()); assertEquals(0, fakeRestChannel.responses().get()); assertEquals(0, fakeRestChannel.errors().get()); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java index dd7dda48ae8..a7351ccfe14 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java @@ -73,7 +73,6 @@ public class SecurityServerTransportInterceptorTests extends ESTestCase { securityContext = spy(new SecurityContext(settings, threadPool.getThreadContext())); xPackLicenseState = mock(XPackLicenseState.class); when(xPackLicenseState.isAuthAllowed()).thenReturn(true); - when(xPackLicenseState.isSecurityEnabled()).thenReturn(true); } @After @@ -102,7 +101,6 @@ public class SecurityServerTransportInterceptorTests extends ESTestCase { sender.sendRequest(null, null, null, null, null); assertTrue(calledWrappedSender.get()); verify(xPackLicenseState).isAuthAllowed(); - verify(xPackLicenseState).isSecurityEnabled(); verifyNoMoreInteractions(xPackLicenseState); verifyZeroInteractions(securityContext); } @@ -112,10 +110,8 @@ public class SecurityServerTransportInterceptorTests extends ESTestCase { mock(AuthenticationService.class), mock(AuthorizationService.class), xPackLicenseState, mock(SSLService.class), securityContext, new DestructiveOperations(Settings.EMPTY, new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING))), clusterService); - final boolean securityEnabled = randomBoolean(); - final boolean authAllowed = securityEnabled && randomBoolean(); + final boolean authAllowed = randomBoolean(); when(xPackLicenseState.isAuthAllowed()).thenReturn(authAllowed); - when(xPackLicenseState.isSecurityEnabled()).thenReturn(securityEnabled); ClusterState notRecovered = ClusterState.builder(clusterService.state()) .blocks(ClusterBlocks.builder().addGlobalBlock(GatewayService.STATE_NOT_RECOVERED_BLOCK).build()) .build(); @@ -139,10 +135,7 @@ public class SecurityServerTransportInterceptorTests extends ESTestCase { sender.sendRequest(connection, "internal:foo", null, null, null); assertTrue(calledWrappedSender.get()); assertEquals(SystemUser.INSTANCE, sendingUser.get()); - verify(xPackLicenseState).isSecurityEnabled(); - if (securityEnabled) { - verify(xPackLicenseState).isAuthAllowed(); - } + verify(xPackLicenseState).isAuthAllowed(); verify(securityContext).executeAsUser(any(User.class), any(Consumer.class), eq(Version.CURRENT)); verifyNoMoreInteractions(xPackLicenseState); } @@ -177,7 +170,6 @@ public class SecurityServerTransportInterceptorTests extends ESTestCase { assertEquals(user, sendingUser.get()); assertEquals(user, securityContext.getUser()); verify(xPackLicenseState).isAuthAllowed(); - verify(xPackLicenseState).isSecurityEnabled(); verify(securityContext, never()).executeAsUser(any(User.class), any(Consumer.class), any(Version.class)); verifyNoMoreInteractions(xPackLicenseState); } @@ -215,7 +207,6 @@ public class SecurityServerTransportInterceptorTests extends ESTestCase { assertEquals(SystemUser.INSTANCE, sendingUser.get()); assertEquals(user, securityContext.getUser()); verify(xPackLicenseState).isAuthAllowed(); - verify(xPackLicenseState).isSecurityEnabled(); verify(securityContext).executeAsUser(any(User.class), any(Consumer.class), eq(Version.CURRENT)); verifyNoMoreInteractions(xPackLicenseState); } @@ -246,7 +237,6 @@ public class SecurityServerTransportInterceptorTests extends ESTestCase { assertEquals("there should always be a user when sending a message for action [indices:foo]", e.getMessage()); assertNull(securityContext.getUser()); verify(xPackLicenseState).isAuthAllowed(); - verify(xPackLicenseState).isSecurityEnabled(); verify(securityContext, never()).executeAsUser(any(User.class), any(Consumer.class), any(Version.class)); verifyNoMoreInteractions(xPackLicenseState); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/filter/IPFilterTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/filter/IPFilterTests.java index 0ff313ceb25..78825d95ce0 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/filter/IPFilterTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/filter/IPFilterTests.java @@ -53,7 +53,6 @@ public class IPFilterTests extends ESTestCase { public void init() { licenseState = mock(XPackLicenseState.class); when(licenseState.isIpFilteringAllowed()).thenReturn(true); - when(licenseState.isSecurityEnabled()).thenReturn(true); auditTrail = mock(AuditTrailService.class); clusterSettings = new ClusterSettings(Settings.EMPTY, new HashSet<>(Arrays.asList( IPFilter.HTTP_FILTER_ALLOW_SETTING, diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/IpFilterRemoteAddressFilterTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/IpFilterRemoteAddressFilterTests.java index 1b45fad8989..ee40d3e24bb 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/IpFilterRemoteAddressFilterTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/IpFilterRemoteAddressFilterTests.java @@ -57,7 +57,6 @@ public class IpFilterRemoteAddressFilterTests extends ESTestCase { IPFilter.PROFILE_FILTER_DENY_SETTING))); XPackLicenseState licenseState = mock(XPackLicenseState.class); when(licenseState.isIpFilteringAllowed()).thenReturn(true); - when(licenseState.isSecurityEnabled()).thenReturn(true); AuditTrailService auditTrailService = new AuditTrailService(settings, Collections.emptyList(), licenseState); IPFilter ipFilter = new IPFilter(settings, auditTrailService, clusterSettings, licenseState); ipFilter.setBoundTransportAddress(transport.boundAddress(), transport.profileBoundAddresses()); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/nio/NioIPFilterTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/nio/NioIPFilterTests.java index 1832669fce1..398b783f642 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/nio/NioIPFilterTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/nio/NioIPFilterTests.java @@ -58,7 +58,6 @@ public class NioIPFilterTests extends ESTestCase { IPFilter.PROFILE_FILTER_DENY_SETTING))); XPackLicenseState licenseState = mock(XPackLicenseState.class); when(licenseState.isIpFilteringAllowed()).thenReturn(true); - when(licenseState.isSecurityEnabled()).thenReturn(true); AuditTrailService auditTrailService = new AuditTrailService(settings, Collections.emptyList(), licenseState); IPFilter ipFilter = new IPFilter(settings, auditTrailService, clusterSettings, licenseState); ipFilter.setBoundTransportAddress(transport.boundAddress(), transport.profileBoundAddresses()); From b5d8495789f7637f96bfba0c1bb528a21971a495 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Wed, 12 Sep 2018 21:50:22 +0200 Subject: [PATCH 42/78] [CCR] Add auto follow pattern APIs to transport client. (#33629) --- .../java/org/elasticsearch/xpack/ccr/Ccr.java | 4 +-- .../ccr/action/AutoFollowCoordinator.java | 2 +- ...ransportDeleteAutoFollowPatternAction.java | 1 + .../TransportPutAutoFollowPatternAction.java | 1 + .../RestDeleteAutoFollowPatternAction.java | 4 +-- .../rest/RestPutAutoFollowPatternAction.java | 4 +-- .../elasticsearch/xpack/ccr/CcrLicenseIT.java | 2 +- .../xpack/ccr/action/AutoFollowTests.java | 2 ++ ...ortDeleteAutoFollowPatternActionTests.java | 2 +- ...nsportPutAutoFollowPatternActionTests.java | 1 + .../xpack/core/ccr/AutoFollowMetadata.java | 28 +++++++++---------- .../action/DeleteAutoFollowPatternAction.java | 2 +- .../action/PutAutoFollowPatternAction.java | 22 +++++++-------- .../xpack/core/ccr/client/CcrClient.java | 26 +++++++++++++++++ .../DeleteAutoFollowPatternRequestTests.java | 2 +- .../PutAutoFollowPatternRequestTests.java | 2 +- 16 files changed, 68 insertions(+), 37 deletions(-) rename x-pack/plugin/{ccr/src/main/java/org/elasticsearch/xpack => core/src/main/java/org/elasticsearch/xpack/core}/ccr/action/DeleteAutoFollowPatternAction.java (98%) rename x-pack/plugin/{ccr/src/main/java/org/elasticsearch/xpack => core/src/main/java/org/elasticsearch/xpack/core}/ccr/action/PutAutoFollowPatternAction.java (91%) rename x-pack/plugin/{ccr/src/test/java/org/elasticsearch/xpack => core/src/test/java/org/elasticsearch/xpack/core}/ccr/action/DeleteAutoFollowPatternRequestTests.java (94%) rename x-pack/plugin/{ccr/src/test/java/org/elasticsearch/xpack => core/src/test/java/org/elasticsearch/xpack/core}/ccr/action/PutAutoFollowPatternRequestTests.java (97%) diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java index 6220ec07e4b..4e4caf8500f 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java @@ -40,8 +40,8 @@ import org.elasticsearch.threadpool.FixedExecutorBuilder; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.ccr.action.AutoFollowCoordinator; -import org.elasticsearch.xpack.ccr.action.DeleteAutoFollowPatternAction; -import org.elasticsearch.xpack.ccr.action.PutAutoFollowPatternAction; +import org.elasticsearch.xpack.core.ccr.action.DeleteAutoFollowPatternAction; +import org.elasticsearch.xpack.core.ccr.action.PutAutoFollowPatternAction; import org.elasticsearch.xpack.ccr.action.ShardChangesAction; import org.elasticsearch.xpack.ccr.action.ShardFollowTask; import org.elasticsearch.xpack.ccr.action.ShardFollowTasksExecutor; diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinator.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinator.java index bc62e439538..722cbddde18 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinator.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinator.java @@ -218,7 +218,7 @@ public class AutoFollowCoordinator implements ClusterStateApplier { new FollowIndexAction.Request(leaderIndexNameWithClusterAliasPrefix, followIndexName, autoFollowPattern.getMaxBatchOperationCount(), autoFollowPattern.getMaxConcurrentReadBatches(), autoFollowPattern.getMaxOperationSizeInBytes(), autoFollowPattern.getMaxConcurrentWriteBatches(), - autoFollowPattern.getMaxWriteBufferSize(), autoFollowPattern.getRetryTimeout(), + autoFollowPattern.getMaxWriteBufferSize(), autoFollowPattern.getMaxRetryDelay(), autoFollowPattern.getIdleShardRetryDelay()); // Execute if the create and follow api call succeeds: diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportDeleteAutoFollowPatternAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportDeleteAutoFollowPatternAction.java index 6c1ca81e7c4..8d2e59defd8 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportDeleteAutoFollowPatternAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportDeleteAutoFollowPatternAction.java @@ -23,6 +23,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.ccr.AutoFollowMetadata; import org.elasticsearch.xpack.core.ccr.AutoFollowMetadata.AutoFollowPattern; +import org.elasticsearch.xpack.core.ccr.action.DeleteAutoFollowPatternAction; import java.util.HashMap; import java.util.List; diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportPutAutoFollowPatternAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportPutAutoFollowPatternAction.java index 0824dea67f6..4afd51f56e6 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportPutAutoFollowPatternAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportPutAutoFollowPatternAction.java @@ -27,6 +27,7 @@ import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.ccr.CcrLicenseChecker; import org.elasticsearch.xpack.core.ccr.AutoFollowMetadata; import org.elasticsearch.xpack.core.ccr.AutoFollowMetadata.AutoFollowPattern; +import org.elasticsearch.xpack.core.ccr.action.PutAutoFollowPatternAction; import java.util.ArrayList; import java.util.HashMap; diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestDeleteAutoFollowPatternAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestDeleteAutoFollowPatternAction.java index d25e9bf65fd..91a607de27b 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestDeleteAutoFollowPatternAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestDeleteAutoFollowPatternAction.java @@ -11,11 +11,11 @@ import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.action.RestToXContentListener; -import org.elasticsearch.xpack.ccr.action.DeleteAutoFollowPatternAction.Request; +import org.elasticsearch.xpack.core.ccr.action.DeleteAutoFollowPatternAction.Request; import java.io.IOException; -import static org.elasticsearch.xpack.ccr.action.DeleteAutoFollowPatternAction.INSTANCE; +import static org.elasticsearch.xpack.core.ccr.action.DeleteAutoFollowPatternAction.INSTANCE; public class RestDeleteAutoFollowPatternAction extends BaseRestHandler { diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestPutAutoFollowPatternAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestPutAutoFollowPatternAction.java index 9b3aac3bbb5..6b9a4aeff20 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestPutAutoFollowPatternAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/rest/RestPutAutoFollowPatternAction.java @@ -12,11 +12,11 @@ import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.action.RestToXContentListener; -import org.elasticsearch.xpack.ccr.action.PutAutoFollowPatternAction.Request; +import org.elasticsearch.xpack.core.ccr.action.PutAutoFollowPatternAction.Request; import java.io.IOException; -import static org.elasticsearch.xpack.ccr.action.PutAutoFollowPatternAction.INSTANCE; +import static org.elasticsearch.xpack.core.ccr.action.PutAutoFollowPatternAction.INSTANCE; public class RestPutAutoFollowPatternAction extends BaseRestHandler { diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/CcrLicenseIT.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/CcrLicenseIT.java index f791be6a633..d8bf2872547 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/CcrLicenseIT.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/CcrLicenseIT.java @@ -25,9 +25,9 @@ import org.elasticsearch.xpack.ccr.action.AutoFollowCoordinator; import org.elasticsearch.xpack.core.ccr.action.CcrStatsAction; import org.elasticsearch.xpack.core.ccr.action.CreateAndFollowIndexAction; import org.elasticsearch.xpack.core.ccr.action.FollowIndexAction; -import org.elasticsearch.xpack.ccr.action.PutAutoFollowPatternAction; import org.elasticsearch.xpack.core.ccr.AutoFollowMetadata; import org.elasticsearch.xpack.core.ccr.AutoFollowMetadata.AutoFollowPattern; +import org.elasticsearch.xpack.core.ccr.action.PutAutoFollowPatternAction; import java.util.Collection; import java.util.Collections; diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/AutoFollowTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/AutoFollowTests.java index f4bd8a69e3f..514c233188a 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/AutoFollowTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/AutoFollowTests.java @@ -15,6 +15,8 @@ import org.elasticsearch.persistent.PersistentTasksCustomMetaData; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESSingleNodeTestCase; import org.elasticsearch.xpack.ccr.LocalStateCcr; +import org.elasticsearch.xpack.core.ccr.action.DeleteAutoFollowPatternAction; +import org.elasticsearch.xpack.core.ccr.action.PutAutoFollowPatternAction; import java.util.Arrays; import java.util.Collection; diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/TransportDeleteAutoFollowPatternActionTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/TransportDeleteAutoFollowPatternActionTests.java index 03065ea8d38..303133d3d82 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/TransportDeleteAutoFollowPatternActionTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/TransportDeleteAutoFollowPatternActionTests.java @@ -10,7 +10,7 @@ import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xpack.ccr.action.DeleteAutoFollowPatternAction.Request; +import org.elasticsearch.xpack.core.ccr.action.DeleteAutoFollowPatternAction.Request; import org.elasticsearch.xpack.core.ccr.AutoFollowMetadata; import java.util.ArrayList; diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/TransportPutAutoFollowPatternActionTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/TransportPutAutoFollowPatternActionTests.java index d894eda0b11..6e7341154c8 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/TransportPutAutoFollowPatternActionTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/TransportPutAutoFollowPatternActionTests.java @@ -12,6 +12,7 @@ import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.ccr.AutoFollowMetadata; +import org.elasticsearch.xpack.core.ccr.action.PutAutoFollowPatternAction; import java.util.ArrayList; import java.util.Arrays; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/AutoFollowMetadata.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/AutoFollowMetadata.java index 244a5d441d9..71fd13d0b50 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/AutoFollowMetadata.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/AutoFollowMetadata.java @@ -169,7 +169,7 @@ public class AutoFollowMetadata extends AbstractNamedDiffable i public static final ParseField MAX_BATCH_SIZE_IN_BYTES = new ParseField("max_batch_size_in_bytes"); public static final ParseField MAX_CONCURRENT_WRITE_BATCHES = new ParseField("max_concurrent_write_batches"); public static final ParseField MAX_WRITE_BUFFER_SIZE = new ParseField("max_write_buffer_size"); - public static final ParseField RETRY_TIMEOUT = new ParseField("retry_timeout"); + public static final ParseField MAX_RETRY_DELAY = new ParseField("retry_timeout"); public static final ParseField IDLE_SHARD_RETRY_DELAY = new ParseField("idle_shard_retry_delay"); @SuppressWarnings("unchecked") @@ -187,8 +187,8 @@ public class AutoFollowMetadata extends AbstractNamedDiffable i PARSER.declareInt(ConstructingObjectParser.optionalConstructorArg(), MAX_CONCURRENT_WRITE_BATCHES); PARSER.declareInt(ConstructingObjectParser.optionalConstructorArg(), MAX_WRITE_BUFFER_SIZE); PARSER.declareField(ConstructingObjectParser.optionalConstructorArg(), - (p, c) -> TimeValue.parseTimeValue(p.text(), RETRY_TIMEOUT.getPreferredName()), - RETRY_TIMEOUT, ObjectParser.ValueType.STRING); + (p, c) -> TimeValue.parseTimeValue(p.text(), MAX_RETRY_DELAY.getPreferredName()), + MAX_RETRY_DELAY, ObjectParser.ValueType.STRING); PARSER.declareField(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> TimeValue.parseTimeValue(p.text(), IDLE_SHARD_RETRY_DELAY.getPreferredName()), IDLE_SHARD_RETRY_DELAY, ObjectParser.ValueType.STRING); @@ -201,12 +201,12 @@ public class AutoFollowMetadata extends AbstractNamedDiffable i private final Long maxOperationSizeInBytes; private final Integer maxConcurrentWriteBatches; private final Integer maxWriteBufferSize; - private final TimeValue retryTimeout; + private final TimeValue maxRetryDelay; private final TimeValue idleShardRetryDelay; public AutoFollowPattern(List leaderIndexPatterns, String followIndexPattern, Integer maxBatchOperationCount, Integer maxConcurrentReadBatches, Long maxOperationSizeInBytes, Integer maxConcurrentWriteBatches, - Integer maxWriteBufferSize, TimeValue retryTimeout, TimeValue idleShardRetryDelay) { + Integer maxWriteBufferSize, TimeValue maxRetryDelay, TimeValue idleShardRetryDelay) { this.leaderIndexPatterns = leaderIndexPatterns; this.followIndexPattern = followIndexPattern; this.maxBatchOperationCount = maxBatchOperationCount; @@ -214,7 +214,7 @@ public class AutoFollowMetadata extends AbstractNamedDiffable i this.maxOperationSizeInBytes = maxOperationSizeInBytes; this.maxConcurrentWriteBatches = maxConcurrentWriteBatches; this.maxWriteBufferSize = maxWriteBufferSize; - this.retryTimeout = retryTimeout; + this.maxRetryDelay = maxRetryDelay; this.idleShardRetryDelay = idleShardRetryDelay; } @@ -226,7 +226,7 @@ public class AutoFollowMetadata extends AbstractNamedDiffable i maxOperationSizeInBytes = in.readOptionalLong(); maxConcurrentWriteBatches = in.readOptionalVInt(); maxWriteBufferSize = in.readOptionalVInt(); - retryTimeout = in.readOptionalTimeValue(); + maxRetryDelay = in.readOptionalTimeValue(); idleShardRetryDelay = in.readOptionalTimeValue(); } @@ -266,8 +266,8 @@ public class AutoFollowMetadata extends AbstractNamedDiffable i return maxWriteBufferSize; } - public TimeValue getRetryTimeout() { - return retryTimeout; + public TimeValue getMaxRetryDelay() { + return maxRetryDelay; } public TimeValue getIdleShardRetryDelay() { @@ -283,7 +283,7 @@ public class AutoFollowMetadata extends AbstractNamedDiffable i out.writeOptionalLong(maxOperationSizeInBytes); out.writeOptionalVInt(maxConcurrentWriteBatches); out.writeOptionalVInt(maxWriteBufferSize); - out.writeOptionalTimeValue(retryTimeout); + out.writeOptionalTimeValue(maxRetryDelay); out.writeOptionalTimeValue(idleShardRetryDelay); } @@ -308,8 +308,8 @@ public class AutoFollowMetadata extends AbstractNamedDiffable i if (maxWriteBufferSize != null){ builder.field(MAX_WRITE_BUFFER_SIZE.getPreferredName(), maxWriteBufferSize); } - if (retryTimeout != null) { - builder.field(RETRY_TIMEOUT.getPreferredName(), retryTimeout); + if (maxRetryDelay != null) { + builder.field(MAX_RETRY_DELAY.getPreferredName(), maxRetryDelay); } if (idleShardRetryDelay != null) { builder.field(IDLE_SHARD_RETRY_DELAY.getPreferredName(), idleShardRetryDelay); @@ -334,7 +334,7 @@ public class AutoFollowMetadata extends AbstractNamedDiffable i Objects.equals(maxOperationSizeInBytes, that.maxOperationSizeInBytes) && Objects.equals(maxConcurrentWriteBatches, that.maxConcurrentWriteBatches) && Objects.equals(maxWriteBufferSize, that.maxWriteBufferSize) && - Objects.equals(retryTimeout, that.retryTimeout) && + Objects.equals(maxRetryDelay, that.maxRetryDelay) && Objects.equals(idleShardRetryDelay, that.idleShardRetryDelay); } @@ -348,7 +348,7 @@ public class AutoFollowMetadata extends AbstractNamedDiffable i maxOperationSizeInBytes, maxConcurrentWriteBatches, maxWriteBufferSize, - retryTimeout, + maxRetryDelay, idleShardRetryDelay ); } diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/DeleteAutoFollowPatternAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/DeleteAutoFollowPatternAction.java similarity index 98% rename from x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/DeleteAutoFollowPatternAction.java rename to x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/DeleteAutoFollowPatternAction.java index 82e142202d2..6d49a370a34 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/DeleteAutoFollowPatternAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/DeleteAutoFollowPatternAction.java @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -package org.elasticsearch.xpack.ccr.action; +package org.elasticsearch.xpack.core.ccr.action; import org.elasticsearch.action.Action; import org.elasticsearch.action.ActionRequestValidationException; diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/PutAutoFollowPatternAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/PutAutoFollowPatternAction.java similarity index 91% rename from x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/PutAutoFollowPatternAction.java rename to x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/PutAutoFollowPatternAction.java index eb23244722d..dc69795bb4a 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/PutAutoFollowPatternAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/PutAutoFollowPatternAction.java @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -package org.elasticsearch.xpack.ccr.action; +package org.elasticsearch.xpack.core.ccr.action; import org.elasticsearch.action.Action; import org.elasticsearch.action.ActionRequestValidationException; @@ -57,11 +57,11 @@ public class PutAutoFollowPatternAction extends Action { PARSER.declareInt(Request::setMaxConcurrentWriteBatches, AutoFollowPattern.MAX_CONCURRENT_WRITE_BATCHES); PARSER.declareInt(Request::setMaxWriteBufferSize, AutoFollowPattern.MAX_WRITE_BUFFER_SIZE); PARSER.declareField(Request::setMaxRetryDelay, - (p, c) -> TimeValue.parseTimeValue(p.text(), AutoFollowPattern.RETRY_TIMEOUT.getPreferredName()), - ShardFollowTask.MAX_RETRY_DELAY, ObjectParser.ValueType.STRING); + (p, c) -> TimeValue.parseTimeValue(p.text(), AutoFollowPattern.MAX_RETRY_DELAY.getPreferredName()), + AutoFollowPattern.MAX_RETRY_DELAY, ObjectParser.ValueType.STRING); PARSER.declareField(Request::setIdleShardRetryDelay, (p, c) -> TimeValue.parseTimeValue(p.text(), AutoFollowPattern.IDLE_SHARD_RETRY_DELAY.getPreferredName()), - ShardFollowTask.IDLE_SHARD_RETRY_DELAY, ObjectParser.ValueType.STRING); + AutoFollowPattern.IDLE_SHARD_RETRY_DELAY, ObjectParser.ValueType.STRING); } public static Request fromXContent(XContentParser parser, String remoteClusterAlias) throws IOException { @@ -222,25 +222,25 @@ public class PutAutoFollowPatternAction extends Action { builder.field(FOLLOW_INDEX_NAME_PATTERN_FIELD.getPreferredName(), followIndexNamePattern); } if (maxBatchOperationCount != null) { - builder.field(ShardFollowTask.MAX_BATCH_OPERATION_COUNT.getPreferredName(), maxBatchOperationCount); + builder.field(AutoFollowPattern.MAX_BATCH_OPERATION_COUNT.getPreferredName(), maxBatchOperationCount); } if (maxOperationSizeInBytes != null) { - builder.field(ShardFollowTask.MAX_BATCH_SIZE_IN_BYTES.getPreferredName(), maxOperationSizeInBytes); + builder.field(AutoFollowPattern.MAX_BATCH_SIZE_IN_BYTES.getPreferredName(), maxOperationSizeInBytes); } if (maxWriteBufferSize != null) { - builder.field(ShardFollowTask.MAX_WRITE_BUFFER_SIZE.getPreferredName(), maxWriteBufferSize); + builder.field(AutoFollowPattern.MAX_WRITE_BUFFER_SIZE.getPreferredName(), maxWriteBufferSize); } if (maxConcurrentReadBatches != null) { - builder.field(ShardFollowTask.MAX_CONCURRENT_READ_BATCHES.getPreferredName(), maxConcurrentReadBatches); + builder.field(AutoFollowPattern.MAX_CONCURRENT_READ_BATCHES.getPreferredName(), maxConcurrentReadBatches); } if (maxConcurrentWriteBatches != null) { - builder.field(ShardFollowTask.MAX_CONCURRENT_WRITE_BATCHES.getPreferredName(), maxConcurrentWriteBatches); + builder.field(AutoFollowPattern.MAX_CONCURRENT_WRITE_BATCHES.getPreferredName(), maxConcurrentWriteBatches); } if (maxRetryDelay != null) { - builder.field(ShardFollowTask.MAX_RETRY_DELAY.getPreferredName(), maxRetryDelay.getStringRep()); + builder.field(AutoFollowPattern.MAX_RETRY_DELAY.getPreferredName(), maxRetryDelay.getStringRep()); } if (idleShardRetryDelay != null) { - builder.field(ShardFollowTask.IDLE_SHARD_RETRY_DELAY.getPreferredName(), idleShardRetryDelay.getStringRep()); + builder.field(AutoFollowPattern.IDLE_SHARD_RETRY_DELAY.getPreferredName(), idleShardRetryDelay.getStringRep()); } } builder.endObject(); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/client/CcrClient.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/client/CcrClient.java index 881979e3d79..3100dae9edf 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/client/CcrClient.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/client/CcrClient.java @@ -13,7 +13,9 @@ import org.elasticsearch.action.support.master.AcknowledgedResponse; import org.elasticsearch.client.ElasticsearchClient; import org.elasticsearch.xpack.core.ccr.action.CcrStatsAction; import org.elasticsearch.xpack.core.ccr.action.CreateAndFollowIndexAction; +import org.elasticsearch.xpack.core.ccr.action.DeleteAutoFollowPatternAction; import org.elasticsearch.xpack.core.ccr.action.FollowIndexAction; +import org.elasticsearch.xpack.core.ccr.action.PutAutoFollowPatternAction; import org.elasticsearch.xpack.core.ccr.action.UnfollowIndexAction; import java.util.Objects; @@ -70,4 +72,28 @@ public class CcrClient { return listener; } + public void putAutoFollowPattern( + final PutAutoFollowPatternAction.Request request, + final ActionListener listener) { + client.execute(PutAutoFollowPatternAction.INSTANCE, request, listener); + } + + public ActionFuture putAutoFollowPattern(final PutAutoFollowPatternAction.Request request) { + final PlainActionFuture listener = PlainActionFuture.newFuture(); + client.execute(PutAutoFollowPatternAction.INSTANCE, request, listener); + return listener; + } + + public void deleteAutoFollowPattern( + final DeleteAutoFollowPatternAction.Request request, + final ActionListener listener) { + client.execute(DeleteAutoFollowPatternAction.INSTANCE, request, listener); + } + + public ActionFuture deleteAutoFollowPattern(final DeleteAutoFollowPatternAction.Request request) { + final PlainActionFuture listener = PlainActionFuture.newFuture(); + client.execute(DeleteAutoFollowPatternAction.INSTANCE, request, listener); + return listener; + } + } diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/DeleteAutoFollowPatternRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ccr/action/DeleteAutoFollowPatternRequestTests.java similarity index 94% rename from x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/DeleteAutoFollowPatternRequestTests.java rename to x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ccr/action/DeleteAutoFollowPatternRequestTests.java index 0ca1b3d1278..135e699bb35 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/DeleteAutoFollowPatternRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ccr/action/DeleteAutoFollowPatternRequestTests.java @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -package org.elasticsearch.xpack.ccr.action; +package org.elasticsearch.xpack.core.ccr.action; import org.elasticsearch.test.AbstractStreamableTestCase; diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/PutAutoFollowPatternRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ccr/action/PutAutoFollowPatternRequestTests.java similarity index 97% rename from x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/PutAutoFollowPatternRequestTests.java rename to x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ccr/action/PutAutoFollowPatternRequestTests.java index d6dad3b019c..f11e1885e80 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/PutAutoFollowPatternRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ccr/action/PutAutoFollowPatternRequestTests.java @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -package org.elasticsearch.xpack.ccr.action; +package org.elasticsearch.xpack.core.ccr.action; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentParser; From eb715d52903b9ca1789f8bfdd7158fa53a2bfa90 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Wed, 12 Sep 2018 17:35:06 -0400 Subject: [PATCH 43/78] Add follower index to CCR monitoring and status (#33645) This commit adds the follower index to CCR shard follow task status, and to monitoring. --- .../xpack/ccr/FollowIndexSecurityIT.java | 9 ++++-- .../xpack/ccr/FollowIndexIT.java | 11 +++++--- .../xpack/ccr/action/ShardFollowNodeTask.java | 1 + .../ccr/action/TransportCcrStatsAction.java | 2 +- .../ShardFollowNodeTaskStatusTests.java | 2 ++ .../core/ccr/ShardFollowNodeTaskStatus.java | 28 +++++++++++++++---- .../xpack/core/ccr/action/CcrStatsAction.java | 16 ++--------- .../src/main/resources/monitoring-es.json | 3 ++ .../ccr/CcrStatsMonitoringDocTests.java | 2 ++ 9 files changed, 47 insertions(+), 27 deletions(-) diff --git a/x-pack/plugin/ccr/qa/multi-cluster-with-security/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java b/x-pack/plugin/ccr/qa/multi-cluster-with-security/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java index d1a90a6ccec..43b16727aac 100644 --- a/x-pack/plugin/ccr/qa/multi-cluster-with-security/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java +++ b/x-pack/plugin/ccr/qa/multi-cluster-with-security/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java @@ -82,7 +82,7 @@ public class FollowIndexSecurityIT extends ESRestTestCase { createAndFollowIndex("leader_cluster:" + allowedIndex, allowedIndex); assertBusy(() -> verifyDocuments(client(), allowedIndex, numDocs)); assertThat(countCcrNodeTasks(), equalTo(1)); - assertBusy(() -> verifyCcrMonitoring(allowedIndex)); + assertBusy(() -> verifyCcrMonitoring(allowedIndex, allowedIndex)); assertOK(client().performRequest(new Request("POST", "/" + allowedIndex + "/_ccr/unfollow"))); // Make sure that there are no other ccr relates operations running: assertBusy(() -> { @@ -206,7 +206,7 @@ public class FollowIndexSecurityIT extends ESRestTestCase { return RestStatus.OK.getStatus() == response.getStatusLine().getStatusCode(); } - private static void verifyCcrMonitoring(String expectedLeaderIndex) throws IOException { + private static void verifyCcrMonitoring(String expectedLeaderIndex, String expectedFollowerIndex) throws IOException { ensureYellow(".monitoring-*"); Request request = new Request("GET", "/.monitoring-*/_search"); @@ -222,7 +222,10 @@ public class FollowIndexSecurityIT extends ESRestTestCase { for (int i = 0; i < hits.size(); i++) { Map hit = (Map) hits.get(i); String leaderIndex = (String) XContentMapValues.extractValue("_source.ccr_stats.leader_index", hit); - assertThat(leaderIndex, endsWith(leaderIndex)); + assertThat(leaderIndex, endsWith(expectedLeaderIndex)); + + final String followerIndex = (String) XContentMapValues.extractValue("_source.ccr_stats.follower_index", hit); + assertThat(followerIndex, equalTo(expectedFollowerIndex)); int foundNumberOfOperationsReceived = (int) XContentMapValues.extractValue("_source.ccr_stats.operations_received", hit); diff --git a/x-pack/plugin/ccr/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexIT.java b/x-pack/plugin/ccr/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexIT.java index ccb5e409e8c..5c1c3915044 100644 --- a/x-pack/plugin/ccr/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexIT.java +++ b/x-pack/plugin/ccr/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexIT.java @@ -77,7 +77,7 @@ public class FollowIndexIT extends ESRestTestCase { index(leaderClient, leaderIndexName, Integer.toString(id + 2), "field", id + 2, "filtered_field", "true"); } assertBusy(() -> verifyDocuments(followIndexName, numDocs + 3)); - assertBusy(() -> verifyCcrMonitoring(leaderIndexName)); + assertBusy(() -> verifyCcrMonitoring(leaderIndexName, followIndexName)); } } @@ -107,7 +107,7 @@ public class FollowIndexIT extends ESRestTestCase { ensureYellow("logs-20190101"); verifyDocuments("logs-20190101", 5); }); - assertBusy(() -> verifyCcrMonitoring("logs-20190101")); + assertBusy(() -> verifyCcrMonitoring("logs-20190101", "logs-20190101")); } private static void index(RestClient client, String index, String id, Object... fields) throws IOException { @@ -159,7 +159,7 @@ public class FollowIndexIT extends ESRestTestCase { } } - private static void verifyCcrMonitoring(String expectedLeaderIndex) throws IOException { + private static void verifyCcrMonitoring(final String expectedLeaderIndex, final String expectedFollowerIndex) throws IOException { ensureYellow(".monitoring-*"); Request request = new Request("GET", "/.monitoring-*/_search"); @@ -175,7 +175,10 @@ public class FollowIndexIT extends ESRestTestCase { for (int i = 0; i < hits.size(); i++) { Map hit = (Map) hits.get(i); String leaderIndex = (String) XContentMapValues.extractValue("_source.ccr_stats.leader_index", hit); - assertThat(leaderIndex, endsWith(leaderIndex)); + assertThat(leaderIndex, endsWith(expectedLeaderIndex)); + + final String followerIndex = (String) XContentMapValues.extractValue("_source.ccr_stats.follower_index", hit); + assertThat(followerIndex, equalTo(expectedFollowerIndex)); int foundNumberOfOperationsReceived = (int) XContentMapValues.extractValue("_source.ccr_stats.operations_received", hit); diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTask.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTask.java index c221c097977..f88f21e4072 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTask.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTask.java @@ -418,6 +418,7 @@ public abstract class ShardFollowNodeTask extends AllocatedPersistentTask { } return new ShardFollowNodeTaskStatus( leaderIndex, + params.getFollowShardId().getIndexName(), getFollowShardId().getId(), leaderGlobalCheckpoint, leaderMaxSeqNo, diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportCcrStatsAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportCcrStatsAction.java index f227a56f158..394b42789d1 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportCcrStatsAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportCcrStatsAction.java @@ -106,7 +106,7 @@ public class TransportCcrStatsAction extends TransportTasksAction< final CcrStatsAction.StatsRequest request, final ShardFollowNodeTask task, final ActionListener listener) { - listener.onResponse(new CcrStatsAction.StatsResponse(task.getFollowShardId(), task.getStatus())); + listener.onResponse(new CcrStatsAction.StatsResponse(task.getStatus())); } } diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskStatusTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskStatusTests.java index 2f145e7a98c..d5f2ab7ea08 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskStatusTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskStatusTests.java @@ -33,6 +33,7 @@ public class ShardFollowNodeTaskStatusTests extends AbstractSerializingTestCase< protected ShardFollowNodeTaskStatus createTestInstance() { // if you change this constructor, reflect the changes in the hand-written assertions below return new ShardFollowNodeTaskStatus( + randomAlphaOfLength(4), randomAlphaOfLength(4), randomInt(), randomNonNegativeLong(), @@ -61,6 +62,7 @@ public class ShardFollowNodeTaskStatusTests extends AbstractSerializingTestCase< protected void assertEqualInstances(final ShardFollowNodeTaskStatus expectedInstance, final ShardFollowNodeTaskStatus newInstance) { assertNotSame(expectedInstance, newInstance); assertThat(newInstance.leaderIndex(), equalTo(expectedInstance.leaderIndex())); + assertThat(newInstance.followerIndex(), equalTo(expectedInstance.followerIndex())); assertThat(newInstance.getShardId(), equalTo(expectedInstance.getShardId())); assertThat(newInstance.leaderGlobalCheckpoint(), equalTo(expectedInstance.leaderGlobalCheckpoint())); assertThat(newInstance.leaderMaxSeqNo(), equalTo(expectedInstance.leaderMaxSeqNo())); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/ShardFollowNodeTaskStatus.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/ShardFollowNodeTaskStatus.java index 2f3c4efb9ad..dafb4a5e29f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/ShardFollowNodeTaskStatus.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/ShardFollowNodeTaskStatus.java @@ -34,6 +34,7 @@ public class ShardFollowNodeTaskStatus implements Task.Status { public static final String STATUS_PARSER_NAME = "shard-follow-node-task-status"; private static final ParseField LEADER_INDEX = new ParseField("leader_index"); + private static final ParseField FOLLOWER_INDEX = new ParseField("follower_index"); private static final ParseField SHARD_ID = new ParseField("shard_id"); private static final ParseField LEADER_GLOBAL_CHECKPOINT_FIELD = new ParseField("leader_global_checkpoint"); private static final ParseField LEADER_MAX_SEQ_NO_FIELD = new ParseField("leader_max_seq_no"); @@ -62,16 +63,16 @@ public class ShardFollowNodeTaskStatus implements Task.Status { STATUS_PARSER_NAME, args -> new ShardFollowNodeTaskStatus( (String) args[0], - (int) args[1], - (long) args[2], + (String) args[1], + (int) args[2], (long) args[3], (long) args[4], (long) args[5], (long) args[6], - (int) args[7], + (long) args[7], (int) args[8], (int) args[9], - (long) args[10], + (int) args[10], (long) args[11], (long) args[12], (long) args[13], @@ -81,11 +82,12 @@ public class ShardFollowNodeTaskStatus implements Task.Status { (long) args[17], (long) args[18], (long) args[19], + (long) args[20], new TreeMap<>( - ((List>) args[20]) + ((List>) args[21]) .stream() .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))), - (long) args[21])); + (long) args[22])); public static final String FETCH_EXCEPTIONS_ENTRY_PARSER_NAME = "shard-follow-node-task-status-fetch-exceptions-entry"; @@ -96,6 +98,7 @@ public class ShardFollowNodeTaskStatus implements Task.Status { static { STATUS_PARSER.declareString(ConstructingObjectParser.constructorArg(), LEADER_INDEX); + STATUS_PARSER.declareString(ConstructingObjectParser.constructorArg(), FOLLOWER_INDEX); STATUS_PARSER.declareInt(ConstructingObjectParser.constructorArg(), SHARD_ID); STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), LEADER_GLOBAL_CHECKPOINT_FIELD); STATUS_PARSER.declareLong(ConstructingObjectParser.constructorArg(), LEADER_MAX_SEQ_NO_FIELD); @@ -136,6 +139,12 @@ public class ShardFollowNodeTaskStatus implements Task.Status { return leaderIndex; } + private final String followerIndex; + + public String followerIndex() { + return followerIndex; + } + private final int shardId; public int getShardId() { @@ -264,6 +273,7 @@ public class ShardFollowNodeTaskStatus implements Task.Status { public ShardFollowNodeTaskStatus( final String leaderIndex, + final String followerIndex, final int shardId, final long leaderGlobalCheckpoint, final long leaderMaxSeqNo, @@ -286,6 +296,7 @@ public class ShardFollowNodeTaskStatus implements Task.Status { final NavigableMap fetchExceptions, final long timeSinceLastFetchMillis) { this.leaderIndex = leaderIndex; + this.followerIndex = followerIndex; this.shardId = shardId; this.leaderGlobalCheckpoint = leaderGlobalCheckpoint; this.leaderMaxSeqNo = leaderMaxSeqNo; @@ -311,6 +322,7 @@ public class ShardFollowNodeTaskStatus implements Task.Status { public ShardFollowNodeTaskStatus(final StreamInput in) throws IOException { this.leaderIndex = in.readString(); + this.followerIndex = in.readString(); this.shardId = in.readVInt(); this.leaderGlobalCheckpoint = in.readZLong(); this.leaderMaxSeqNo = in.readZLong(); @@ -342,6 +354,7 @@ public class ShardFollowNodeTaskStatus implements Task.Status { @Override public void writeTo(final StreamOutput out) throws IOException { out.writeString(leaderIndex); + out.writeString(followerIndex); out.writeVInt(shardId); out.writeZLong(leaderGlobalCheckpoint); out.writeZLong(leaderMaxSeqNo); @@ -377,6 +390,7 @@ public class ShardFollowNodeTaskStatus implements Task.Status { public XContentBuilder toXContentFragment(final XContentBuilder builder, final Params params) throws IOException { builder.field(LEADER_INDEX.getPreferredName(), leaderIndex); + builder.field(FOLLOWER_INDEX.getPreferredName(), followerIndex); builder.field(SHARD_ID.getPreferredName(), shardId); builder.field(LEADER_GLOBAL_CHECKPOINT_FIELD.getPreferredName(), leaderGlobalCheckpoint); builder.field(LEADER_MAX_SEQ_NO_FIELD.getPreferredName(), leaderMaxSeqNo); @@ -439,6 +453,7 @@ public class ShardFollowNodeTaskStatus implements Task.Status { if (o == null || getClass() != o.getClass()) return false; final ShardFollowNodeTaskStatus that = (ShardFollowNodeTaskStatus) o; return leaderIndex.equals(that.leaderIndex) && + followerIndex.equals(that.followerIndex) && shardId == that.shardId && leaderGlobalCheckpoint == that.leaderGlobalCheckpoint && leaderMaxSeqNo == that.leaderMaxSeqNo && @@ -471,6 +486,7 @@ public class ShardFollowNodeTaskStatus implements Task.Status { public int hashCode() { return Objects.hash( leaderIndex, + followerIndex, shardId, leaderGlobalCheckpoint, leaderMaxSeqNo, diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/CcrStatsAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/CcrStatsAction.java index 1074b6905d3..863cb678d7e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/CcrStatsAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/CcrStatsAction.java @@ -19,7 +19,6 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.tasks.Task; import org.elasticsearch.xpack.core.ccr.ShardFollowNodeTaskStatus; @@ -70,8 +69,8 @@ public class CcrStatsAction extends Action { final Map> taskResponsesByIndex = new TreeMap<>(); for (final StatsResponse statsResponse : statsResponse) { taskResponsesByIndex.computeIfAbsent( - statsResponse.followerShardId().getIndexName(), - k -> new TreeMap<>()).put(statsResponse.followerShardId().getId(), statsResponse); + statsResponse.status().followerIndex(), + k -> new TreeMap<>()).put(statsResponse.status().getShardId(), statsResponse); } builder.startObject(); { @@ -150,31 +149,22 @@ public class CcrStatsAction extends Action { public static class StatsResponse implements Writeable { - private final ShardId followerShardId; - - public ShardId followerShardId() { - return followerShardId; - } - private final ShardFollowNodeTaskStatus status; public ShardFollowNodeTaskStatus status() { return status; } - public StatsResponse(final ShardId followerShardId, final ShardFollowNodeTaskStatus status) { - this.followerShardId = followerShardId; + public StatsResponse(final ShardFollowNodeTaskStatus status) { this.status = status; } public StatsResponse(final StreamInput in) throws IOException { - this.followerShardId = ShardId.readShardId(in); this.status = new ShardFollowNodeTaskStatus(in); } @Override public void writeTo(final StreamOutput out) throws IOException { - followerShardId.writeTo(out); status.writeTo(out); } diff --git a/x-pack/plugin/core/src/main/resources/monitoring-es.json b/x-pack/plugin/core/src/main/resources/monitoring-es.json index 9cca4a6e248..83c9fe70e11 100644 --- a/x-pack/plugin/core/src/main/resources/monitoring-es.json +++ b/x-pack/plugin/core/src/main/resources/monitoring-es.json @@ -922,6 +922,9 @@ "leader_index": { "type": "keyword" }, + "follower_index": { + "type": "keyword" + }, "shard_id": { "type": "integer" }, diff --git a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsMonitoringDocTests.java b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsMonitoringDocTests.java index 47f2bdf5d2e..70b73e5eed0 100644 --- a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsMonitoringDocTests.java +++ b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsMonitoringDocTests.java @@ -98,6 +98,7 @@ public class CcrStatsMonitoringDocTests extends BaseMonitoringDocTestCase Date: Wed, 12 Sep 2018 15:56:13 -0700 Subject: [PATCH 44/78] [CCR Monitoring] Only collect stats for specified indices (#33646) Follow up to #33617. Relates to #30086. As with all other per-index Monitoring collectors, the `CcrStatsCollector` should only collect stats for the indices the user wants to monitor. This list is controlled by the `xpack.monitoring.collection.indices` setting and defaults to all indices. --- .../xpack/monitoring/collector/ccr/CcrStatsCollector.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsCollector.java b/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsCollector.java index fbb7505af4d..510f430d196 100644 --- a/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsCollector.java +++ b/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsCollector.java @@ -6,10 +6,10 @@ package org.elasticsearch.xpack.monitoring.collector.ccr; +import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.client.Client; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.service.ClusterService; -import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; @@ -72,7 +72,8 @@ public class CcrStatsCollector extends Collector { final ClusterState clusterState) throws Exception { try (ThreadContext.StoredContext ignore = stashWithOrigin(threadContext, MONITORING_ORIGIN)) { final CcrStatsAction.StatsRequest request = new CcrStatsAction.StatsRequest(); - request.setIndices(Strings.EMPTY_ARRAY); + request.setIndices(getCollectionIndices()); + request.setIndicesOptions(IndicesOptions.lenientExpandOpen()); final CcrStatsAction.StatsResponses responses = ccrClient.stats(request).actionGet(getCollectionTimeout()); final long timestamp = timestamp(); From b097eff34244d6b046cdadfec84e8fc9f1af6714 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Wed, 12 Sep 2018 21:27:59 -0400 Subject: [PATCH 45/78] Resync fails to notify on unavaiable exceptions (#33615) We fail to notify the resync listener if the resync replication hits a shard unavailable exception. Moreover, we no longer need to swallow these unavailable exceptions. Relates #28571 Closes #33613 --- .../action/resync/TransportResyncReplicationAction.java | 8 +------- .../org/elasticsearch/gateway/GatewayIndexStateIT.java | 1 - 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/resync/TransportResyncReplicationAction.java b/server/src/main/java/org/elasticsearch/action/resync/TransportResyncReplicationAction.java index f8ad58b9cac..7881e57200b 100644 --- a/server/src/main/java/org/elasticsearch/action/resync/TransportResyncReplicationAction.java +++ b/server/src/main/java/org/elasticsearch/action/resync/TransportResyncReplicationAction.java @@ -22,7 +22,6 @@ import org.apache.logging.log4j.message.ParameterizedMessage; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; -import org.elasticsearch.action.support.TransportActions; import org.elasticsearch.action.support.replication.ReplicationOperation; import org.elasticsearch.action.support.replication.ReplicationResponse; import org.elasticsearch.action.support.replication.TransportReplicationAction; @@ -171,12 +170,7 @@ public class TransportResyncReplicationAction extends TransportWriteAction Date: Thu, 13 Sep 2018 07:24:51 +0200 Subject: [PATCH 46/78] Mark NamedDateTimeProcessorTests as @AwaitsFix --- .../function/scalar/datetime/NamedDateTimeProcessorTests.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NamedDateTimeProcessorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NamedDateTimeProcessorTests.java index 828a16f5aa9..6566a2fbd88 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NamedDateTimeProcessorTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NamedDateTimeProcessorTests.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.sql.expression.function.scalar.datetime; +import org.apache.lucene.util.LuceneTestCase.AwaitsFix; import org.elasticsearch.common.io.stream.Writeable.Reader; import org.elasticsearch.test.AbstractWireSerializingTestCase; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.NamedDateTimeProcessor.NameExtractor; @@ -14,6 +15,7 @@ import org.joda.time.DateTimeZone; import java.io.IOException; import java.util.TimeZone; +@AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/33621") public class NamedDateTimeProcessorTests extends AbstractWireSerializingTestCase { private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); From 94f6d4560dfc0898137c19d717d43907316c0857 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 13 Sep 2018 07:32:33 +0200 Subject: [PATCH 47/78] Revert "Mark NamedDateTimeProcessorTests as @AwaitsFix" This reverts commit 44a80d61b287b48e4e2a5782bfdb008d552858aa. --- .../function/scalar/datetime/NamedDateTimeProcessorTests.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NamedDateTimeProcessorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NamedDateTimeProcessorTests.java index 6566a2fbd88..828a16f5aa9 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NamedDateTimeProcessorTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/NamedDateTimeProcessorTests.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.xpack.sql.expression.function.scalar.datetime; -import org.apache.lucene.util.LuceneTestCase.AwaitsFix; import org.elasticsearch.common.io.stream.Writeable.Reader; import org.elasticsearch.test.AbstractWireSerializingTestCase; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.NamedDateTimeProcessor.NameExtractor; @@ -15,7 +14,6 @@ import org.joda.time.DateTimeZone; import java.io.IOException; import java.util.TimeZone; -@AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/33621") public class NamedDateTimeProcessorTests extends AbstractWireSerializingTestCase { private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); From 5a3fd8e4e7c8d2721b3542ffa859c9245678d6c2 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 13 Sep 2018 07:37:15 +0200 Subject: [PATCH 48/78] Use file-based discovery not MockUncasedHostsProvider (#33554) Today we use a special unicast hosts provider, the `MockUncasedHostsProvider`, in many integration tests, to deal with the dynamic nature of the allocation of ports to nodes. However #33241 allows us to use file-based discovery to achieve the same goal, so the special test-only `MockUncasedHostsProvider` is no longer required. This change removes `MockUncasedHostProvider` and replaces it with file-based discovery in tests based on `EsIntegTestCase`. --- .../discovery/DiscoveryModule.java | 2 +- .../cluster/ClusterInfoServiceIT.java | 7 +- .../zen/SettingsBasedHostProviderIT.java | 81 +++++++++++++++ .../search/SearchCancellationIT.java | 5 +- .../elasticsearch/test/ESIntegTestCase.java | 11 ++- .../test/ESSingleNodeTestCase.java | 2 + .../test/InternalTestCluster.java | 98 +++++++++++++------ .../discovery/MockUncasedHostProvider.java | 91 ----------------- .../test/discovery/TestZenDiscovery.java | 34 ------- .../elasticsearch/license/LicensingTests.java | 11 ++- 10 files changed, 178 insertions(+), 164 deletions(-) create mode 100644 server/src/test/java/org/elasticsearch/discovery/zen/SettingsBasedHostProviderIT.java delete mode 100644 test/framework/src/main/java/org/elasticsearch/test/discovery/MockUncasedHostProvider.java diff --git a/server/src/main/java/org/elasticsearch/discovery/DiscoveryModule.java b/server/src/main/java/org/elasticsearch/discovery/DiscoveryModule.java index f34798605d7..91f4e615159 100644 --- a/server/src/main/java/org/elasticsearch/discovery/DiscoveryModule.java +++ b/server/src/main/java/org/elasticsearch/discovery/DiscoveryModule.java @@ -131,7 +131,7 @@ public class DiscoveryModule { if (discoverySupplier == null) { throw new IllegalArgumentException("Unknown discovery type [" + discoveryType + "]"); } - Loggers.getLogger(getClass(), settings).info("using discovery type [{}]", discoveryType); + Loggers.getLogger(getClass(), settings).info("using discovery type [{}] and host providers {}", discoveryType, hostsProviderNames); discovery = Objects.requireNonNull(discoverySupplier.get()); } diff --git a/server/src/test/java/org/elasticsearch/cluster/ClusterInfoServiceIT.java b/server/src/test/java/org/elasticsearch/cluster/ClusterInfoServiceIT.java index b17a0cc5418..1a0e964ef77 100644 --- a/server/src/test/java/org/elasticsearch/cluster/ClusterInfoServiceIT.java +++ b/server/src/test/java/org/elasticsearch/cluster/ClusterInfoServiceIT.java @@ -53,7 +53,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Set; -import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; import static java.util.Collections.emptySet; @@ -113,6 +112,7 @@ public class ClusterInfoServiceIT extends ESIntegTestCase { @Override protected Settings nodeSettings(int nodeOrdinal) { return Settings.builder() + .put(super.nodeSettings(nodeOrdinal)) // manual collection or upon cluster forming. .put(NodeEnvironment.MAX_LOCAL_STORAGE_NODES_SETTING.getKey(), 2) .put(InternalClusterInfoService.INTERNAL_CLUSTER_INFO_TIMEOUT_SETTING.getKey(), "1s") @@ -121,8 +121,7 @@ public class ClusterInfoServiceIT extends ESIntegTestCase { @Override protected Collection> nodePlugins() { - return Arrays.asList(TestPlugin.class, - MockTransportService.TestPlugin.class); + return Arrays.asList(TestPlugin.class, MockTransportService.TestPlugin.class); } public void testClusterInfoServiceCollectsInformation() throws Exception { @@ -172,7 +171,7 @@ public class ClusterInfoServiceIT extends ESIntegTestCase { } } - public void testClusterInfoServiceInformationClearOnError() throws InterruptedException, ExecutionException { + public void testClusterInfoServiceInformationClearOnError() { internalCluster().startNodes(2, // manually control publishing Settings.builder().put(InternalClusterInfoService.INTERNAL_CLUSTER_INFO_UPDATE_INTERVAL_SETTING.getKey(), "60m").build()); diff --git a/server/src/test/java/org/elasticsearch/discovery/zen/SettingsBasedHostProviderIT.java b/server/src/test/java/org/elasticsearch/discovery/zen/SettingsBasedHostProviderIT.java new file mode 100644 index 00000000000..429950bf853 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/discovery/zen/SettingsBasedHostProviderIT.java @@ -0,0 +1,81 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.discovery.zen; + +import org.elasticsearch.action.admin.cluster.node.info.NodesInfoRequest; +import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.test.ESIntegTestCase; + +import static org.elasticsearch.discovery.DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING; +import static org.elasticsearch.discovery.zen.SettingsBasedHostsProvider.DISCOVERY_ZEN_PING_UNICAST_HOSTS_SETTING; +import static org.elasticsearch.discovery.zen.SettingsBasedHostsProvider.LIMIT_LOCAL_PORTS_COUNT; +import static org.elasticsearch.transport.TcpTransport.PORT; + +@ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST, numDataNodes = 0, numClientNodes = 0) +public class SettingsBasedHostProviderIT extends ESIntegTestCase { + + @Override + protected Settings nodeSettings(int nodeOrdinal) { + Settings.Builder builder = Settings.builder().put(super.nodeSettings(nodeOrdinal)); + + // super.nodeSettings enables file-based discovery, but here we disable it again so we can test the static list: + if (randomBoolean()) { + builder.putList(DISCOVERY_HOSTS_PROVIDER_SETTING.getKey()); + } else { + builder.remove(DISCOVERY_HOSTS_PROVIDER_SETTING.getKey()); + } + + // super.nodeSettings sets this to an empty list, which disables any search for other nodes, but here we want this to happen: + builder.remove(DISCOVERY_ZEN_PING_UNICAST_HOSTS_SETTING.getKey()); + + return builder.build(); + } + + public void testClusterFormsWithSingleSeedHostInSettings() { + final String seedNodeName = internalCluster().startNode(); + final NodesInfoResponse nodesInfoResponse + = client(seedNodeName).admin().cluster().nodesInfo(new NodesInfoRequest("_local")).actionGet(); + final String seedNodeAddress = nodesInfoResponse.getNodes().get(0).getTransport().getAddress().publishAddress().toString(); + logger.info("--> using seed node address {}", seedNodeAddress); + + int extraNodes = randomIntBetween(1, 5); + internalCluster().startNodes(extraNodes, + Settings.builder().putList(DISCOVERY_ZEN_PING_UNICAST_HOSTS_SETTING.getKey(), seedNodeAddress).build()); + + ensureStableCluster(extraNodes + 1); + } + + public void testClusterFormsByScanningPorts() { + // This test will fail if all 4 ports just less than the one used by the first node are already bound by something else. It's hard + // to know how often this might happen in reality, so let's try it and see. + + final String seedNodeName = internalCluster().startNode(); + final NodesInfoResponse nodesInfoResponse + = client(seedNodeName).admin().cluster().nodesInfo(new NodesInfoRequest("_local")).actionGet(); + final int seedNodePort = nodesInfoResponse.getNodes().get(0).getTransport().getAddress().publishAddress().getPort(); + final int minPort = randomIntBetween(seedNodePort - LIMIT_LOCAL_PORTS_COUNT + 1, seedNodePort - 1); + final String portSpec = minPort + "-" + seedNodePort; + + logger.info("--> using port specification [{}]", portSpec); + internalCluster().startNode(Settings.builder().put(PORT.getKey(), portSpec)); + ensureStableCluster(2); + } +} diff --git a/server/src/test/java/org/elasticsearch/search/SearchCancellationIT.java b/server/src/test/java/org/elasticsearch/search/SearchCancellationIT.java index 0294f9f67f8..2e28d16c71d 100644 --- a/server/src/test/java/org/elasticsearch/search/SearchCancellationIT.java +++ b/server/src/test/java/org/elasticsearch/search/SearchCancellationIT.java @@ -69,7 +69,10 @@ public class SearchCancellationIT extends ESIntegTestCase { protected Settings nodeSettings(int nodeOrdinal) { boolean lowLevelCancellation = randomBoolean(); logger.info("Using lowLevelCancellation: {}", lowLevelCancellation); - return Settings.builder().put(SearchService.LOW_LEVEL_CANCELLATION_SETTING.getKey(), lowLevelCancellation).build(); + return Settings.builder() + .put(super.nodeSettings(nodeOrdinal)) + .put(SearchService.LOW_LEVEL_CANCELLATION_SETTING.getKey(), lowLevelCancellation) + .build(); } private void indexTestData() { diff --git a/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java index 52ed2205ab5..52f234c9690 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java @@ -206,6 +206,8 @@ import static org.elasticsearch.client.Requests.syncedFlushRequest; import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_REPLICAS; import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_SHARDS; import static org.elasticsearch.common.util.CollectionUtils.eagerPartition; +import static org.elasticsearch.discovery.DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING; +import static org.elasticsearch.discovery.zen.SettingsBasedHostsProvider.DISCOVERY_ZEN_PING_UNICAST_HOSTS_SETTING; import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; import static org.elasticsearch.test.XContentTestUtils.convertToMap; import static org.elasticsearch.test.XContentTestUtils.differenceBetweenMapsIgnoringArrayOrder; @@ -1808,7 +1810,9 @@ public abstract class ESIntegTestCase extends ESTestCase { // wait short time for other active shards before actually deleting, default 30s not needed in tests .put(IndicesStore.INDICES_STORE_DELETE_SHARD_TIMEOUT.getKey(), new TimeValue(1, TimeUnit.SECONDS)) // randomly enable low-level search cancellation to make sure it does not alter results - .put(SearchService.LOW_LEVEL_CANCELLATION_SETTING.getKey(), randomBoolean()); + .put(SearchService.LOW_LEVEL_CANCELLATION_SETTING.getKey(), randomBoolean()) + .putList(DISCOVERY_ZEN_PING_UNICAST_HOSTS_SETTING.getKey()) // empty list disables a port scan for other nodes + .putList(DISCOVERY_HOSTS_PROVIDER_SETTING.getKey(), "file"); if (rarely()) { // Sometimes adjust the minimum search thread pool size, causing // QueueResizingEsThreadPoolExecutor to be used instead of a regular @@ -1921,7 +1925,7 @@ public abstract class ESIntegTestCase extends ESTestCase { networkSettings.put(NetworkModule.TRANSPORT_TYPE_KEY, getTestTransportType()); } - NodeConfigurationSource nodeConfigurationSource = new NodeConfigurationSource() { + return new NodeConfigurationSource() { @Override public Settings nodeSettings(int nodeOrdinal) { return Settings.builder() @@ -1955,7 +1959,6 @@ public abstract class ESIntegTestCase extends ESTestCase { return Collections.unmodifiableCollection(plugins); } }; - return nodeConfigurationSource; } /** @@ -2029,7 +2032,7 @@ public abstract class ESIntegTestCase extends ESTestCase { public static final class TestSeedPlugin extends Plugin { @Override public List> getSettings() { - return Arrays.asList(INDEX_TEST_SEED_SETTING); + return Collections.singletonList(INDEX_TEST_SEED_SETTING); } } diff --git a/test/framework/src/main/java/org/elasticsearch/test/ESSingleNodeTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/ESSingleNodeTestCase.java index d73520f91b3..bcaa4e8303f 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESSingleNodeTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESSingleNodeTestCase.java @@ -62,6 +62,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import static org.elasticsearch.discovery.zen.SettingsBasedHostsProvider.DISCOVERY_ZEN_PING_UNICAST_HOSTS_SETTING; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.lessThanOrEqualTo; @@ -197,6 +198,7 @@ public abstract class ESSingleNodeTestCase extends ESTestCase { // turning on the real memory circuit breaker leads to spurious test failures. As have no full control over heap usage, we // turn it off for these tests. .put(HierarchyCircuitBreakerService.USE_REAL_MEMORY_USAGE_SETTING.getKey(), false) + .putList(DISCOVERY_ZEN_PING_UNICAST_HOSTS_SETTING.getKey()) // empty list disables a port scan for other nodes .put(nodeSettings()) // allow test cases to provide their own settings or override these .build(); Collection> plugins = getPlugins(); diff --git a/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java b/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java index 08aafaea399..354cb807bb2 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java +++ b/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java @@ -47,6 +47,7 @@ import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Strings; import org.elasticsearch.common.breaker.CircuitBreaker; +import org.elasticsearch.common.component.LifecycleListener; import org.elasticsearch.common.io.FileSystemUtils; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.lease.Releasables; @@ -103,6 +104,7 @@ import java.io.Closeable; import java.io.IOException; import java.io.UncheckedIOException; import java.net.InetSocketAddress; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; @@ -114,6 +116,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NavigableMap; +import java.util.Objects; import java.util.Random; import java.util.Set; import java.util.TreeMap; @@ -128,10 +131,12 @@ import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; +import static java.util.Collections.emptyList; import static org.apache.lucene.util.LuceneTestCase.TEST_NIGHTLY; import static org.apache.lucene.util.LuceneTestCase.rarely; import static org.elasticsearch.discovery.DiscoverySettings.INITIAL_STATE_TIMEOUT_SETTING; import static org.elasticsearch.discovery.zen.ElectMasterService.DISCOVERY_ZEN_MINIMUM_MASTER_NODES_SETTING; +import static org.elasticsearch.discovery.zen.FileBasedUnicastHostsProvider.UNICAST_HOSTS_FILE; import static org.elasticsearch.test.ESTestCase.assertBusy; import static org.elasticsearch.test.ESTestCase.awaitBusy; import static org.elasticsearch.test.ESTestCase.getTestTransportType; @@ -486,11 +491,13 @@ public final class InternalTestCluster extends TestCluster { private synchronized NodeAndClient getOrBuildRandomNode() { ensureOpen(); - NodeAndClient randomNodeAndClient = getRandomNodeAndClient(); + final NodeAndClient randomNodeAndClient = getRandomNodeAndClient(); if (randomNodeAndClient != null) { return randomNodeAndClient; } - NodeAndClient buildNode = buildNode(1); + final int ord = nextNodeId.getAndIncrement(); + final Runnable onTransportServiceStarted = () -> {}; // do not create unicast host file for this one node. + final NodeAndClient buildNode = buildNode(ord, random.nextLong(), null, false, 1, onTransportServiceStarted); buildNode.startNode(); publishNode(buildNode); return buildNode; @@ -562,20 +569,11 @@ public final class InternalTestCluster extends TestCluster { * * @param settings the settings to use * @param defaultMinMasterNodes min_master_nodes value to use if min_master_nodes is auto managed + * @param onTransportServiceStarted callback to run when transport service is started */ - private NodeAndClient buildNode(Settings settings, int defaultMinMasterNodes) { + private NodeAndClient buildNode(Settings settings, int defaultMinMasterNodes, Runnable onTransportServiceStarted) { int ord = nextNodeId.getAndIncrement(); - return buildNode(ord, random.nextLong(), settings, false, defaultMinMasterNodes); - } - - /** - * builds a new node with default settings - * - * @param defaultMinMasterNodes min_master_nodes value to use if min_master_nodes is auto managed - */ - private NodeAndClient buildNode(int defaultMinMasterNodes) { - int ord = nextNodeId.getAndIncrement(); - return buildNode(ord, random.nextLong(), null, false, defaultMinMasterNodes); + return buildNode(ord, random.nextLong(), settings, false, defaultMinMasterNodes, onTransportServiceStarted); } /** @@ -587,15 +585,17 @@ public final class InternalTestCluster extends TestCluster { * @param reuseExisting if a node with the same name is already part of {@link #nodes}, no new node will be built and * the method will return the existing one * @param defaultMinMasterNodes min_master_nodes value to use if min_master_nodes is auto managed + * @param onTransportServiceStarted callback to run when transport service is started */ private NodeAndClient buildNode(int nodeId, long seed, Settings settings, - boolean reuseExisting, int defaultMinMasterNodes) { + boolean reuseExisting, int defaultMinMasterNodes, Runnable onTransportServiceStarted) { assert Thread.holdsLock(this); ensureOpen(); settings = getSettings(nodeId, seed, settings); Collection> plugins = getPlugins(); String name = buildNodeName(nodeId, settings); if (reuseExisting && nodes.containsKey(name)) { + onTransportServiceStarted.run(); // reusing an existing node implies its transport service already started return nodes.get(name); } else { assert reuseExisting == true || nodes.containsKey(name) == false : @@ -631,6 +631,12 @@ public final class InternalTestCluster extends TestCluster { plugins, nodeConfigurationSource.nodeConfigPath(nodeId), forbidPrivateIndexSettings); + node.injector().getInstance(TransportService.class).addLifecycleListener(new LifecycleListener() { + @Override + public void afterStart() { + onTransportServiceStarted.run(); + } + }); try { IOUtils.close(secureSettings); } catch (IOException e) { @@ -907,14 +913,15 @@ public final class InternalTestCluster extends TestCluster { if (!node.isClosed()) { closeNode(); } - recreateNodeOnRestart(callback, clearDataIfNeeded, minMasterNodes); + recreateNodeOnRestart(callback, clearDataIfNeeded, minMasterNodes, () -> rebuildUnicastHostFiles(emptyList())); startNode(); } /** * rebuilds a new node object using the current node settings and starts it */ - void recreateNodeOnRestart(RestartCallback callback, boolean clearDataIfNeeded, int minMasterNodes) throws Exception { + void recreateNodeOnRestart(RestartCallback callback, boolean clearDataIfNeeded, int minMasterNodes, + Runnable onTransportServiceStarted) throws Exception { assert callback != null; Settings callbackSettings = callback.onNodeStopped(name); Settings.Builder newSettings = Settings.builder(); @@ -928,7 +935,7 @@ public final class InternalTestCluster extends TestCluster { if (clearDataIfNeeded) { clearDataIfNeeded(callback); } - createNewNode(newSettings.build()); + createNewNode(newSettings.build(), onTransportServiceStarted); // make sure cached client points to new node resetClient(); } @@ -944,7 +951,7 @@ public final class InternalTestCluster extends TestCluster { } } - private void createNewNode(final Settings newSettings) { + private void createNewNode(final Settings newSettings, final Runnable onTransportServiceStarted) { final long newIdSeed = NodeEnvironment.NODE_ID_SEED_SETTING.get(node.settings()) + 1; // use a new seed to make sure we have new node id Settings finalSettings = Settings.builder().put(node.originalSettings()).put(newSettings).put(NodeEnvironment.NODE_ID_SEED_SETTING.getKey(), newIdSeed).build(); if (DISCOVERY_ZEN_MINIMUM_MASTER_NODES_SETTING.exists(finalSettings) == false) { @@ -953,6 +960,12 @@ public final class InternalTestCluster extends TestCluster { } Collection> plugins = node.getClasspathPlugins(); node = new MockNode(finalSettings, plugins); + node.injector().getInstance(TransportService.class).addLifecycleListener(new LifecycleListener() { + @Override + public void afterStart() { + onTransportServiceStarted.run(); + } + }); markNodeDataDirsAsNotEligableForWipe(node); } @@ -1055,11 +1068,13 @@ public final class InternalTestCluster extends TestCluster { final int numberOfMasterNodes = numSharedDedicatedMasterNodes > 0 ? numSharedDedicatedMasterNodes : numSharedDataNodes; final int defaultMinMasterNodes = (numberOfMasterNodes / 2) + 1; final List toStartAndPublish = new ArrayList<>(); // we want to start nodes in one go due to min master nodes + final Runnable onTransportServiceStarted = () -> rebuildUnicastHostFiles(toStartAndPublish); for (int i = 0; i < numSharedDedicatedMasterNodes; i++) { final Settings.Builder settings = Settings.builder(); settings.put(Node.NODE_MASTER_SETTING.getKey(), true); settings.put(Node.NODE_DATA_SETTING.getKey(), false); - NodeAndClient nodeAndClient = buildNode(i, sharedNodesSeeds[i], settings.build(), true, defaultMinMasterNodes); + NodeAndClient nodeAndClient = buildNode(i, sharedNodesSeeds[i], settings.build(), true, defaultMinMasterNodes, + onTransportServiceStarted); toStartAndPublish.add(nodeAndClient); } for (int i = numSharedDedicatedMasterNodes; i < numSharedDedicatedMasterNodes + numSharedDataNodes; i++) { @@ -1069,14 +1084,16 @@ public final class InternalTestCluster extends TestCluster { settings.put(Node.NODE_MASTER_SETTING.getKey(), false).build(); settings.put(Node.NODE_DATA_SETTING.getKey(), true).build(); } - NodeAndClient nodeAndClient = buildNode(i, sharedNodesSeeds[i], settings.build(), true, defaultMinMasterNodes); + NodeAndClient nodeAndClient = buildNode(i, sharedNodesSeeds[i], settings.build(), true, defaultMinMasterNodes, + onTransportServiceStarted); toStartAndPublish.add(nodeAndClient); } for (int i = numSharedDedicatedMasterNodes + numSharedDataNodes; i < numSharedDedicatedMasterNodes + numSharedDataNodes + numSharedCoordOnlyNodes; i++) { final Builder settings = Settings.builder().put(Node.NODE_MASTER_SETTING.getKey(), false) .put(Node.NODE_DATA_SETTING.getKey(), false).put(Node.NODE_INGEST_SETTING.getKey(), false); - NodeAndClient nodeAndClient = buildNode(i, sharedNodesSeeds[i], settings.build(), true, defaultMinMasterNodes); + NodeAndClient nodeAndClient = buildNode(i, sharedNodesSeeds[i], settings.build(), true, defaultMinMasterNodes, + onTransportServiceStarted); toStartAndPublish.add(nodeAndClient); } @@ -1429,6 +1446,7 @@ public final class InternalTestCluster extends TestCluster { updateMinMasterNodes(currentMasters + newMasters); } List> futures = nodeAndClients.stream().map(node -> executor.submit(node::startNode)).collect(Collectors.toList()); + try { for (Future future : futures) { future.get(); @@ -1449,6 +1467,30 @@ public final class InternalTestCluster extends TestCluster { } } + private final Object discoveryFileMutex = new Object(); + + private void rebuildUnicastHostFiles(Collection newNodes) { + // cannot be a synchronized method since it's called on other threads from within synchronized startAndPublishNodesAndClients() + synchronized (discoveryFileMutex) { + try { + List discoveryFileContents = Stream.concat(nodes.values().stream(), newNodes.stream()) + .map(nac -> nac.node.injector().getInstance(TransportService.class)).filter(Objects::nonNull) + .map(TransportService::getLocalNode).filter(Objects::nonNull).filter(DiscoveryNode::isMasterNode) + .map(n -> n.getAddress().toString()) + .distinct().collect(Collectors.toList()); + Set configPaths = Stream.concat(nodes.values().stream(), newNodes.stream()) + .map(nac -> nac.node.getEnvironment().configFile()).collect(Collectors.toSet()); + logger.debug("configuring discovery with {} at {}", discoveryFileContents, configPaths); + for (final Path configPath : configPaths) { + Files.createDirectories(configPath); + Files.write(configPath.resolve(UNICAST_HOSTS_FILE), discoveryFileContents); + } + } catch (IOException e) { + throw new AssertionError("failed to configure file-based discovery", e); + } + } + } + private synchronized void stopNodesAndClient(NodeAndClient nodeAndClient) throws IOException { stopNodesAndClients(Collections.singleton(nodeAndClient)); } @@ -1607,7 +1649,7 @@ public final class InternalTestCluster extends TestCluster { for (List sameRoleNodes : nodesByRoles.values()) { Collections.shuffle(sameRoleNodes, random); } - List startUpOrder = new ArrayList<>(); + final List startUpOrder = new ArrayList<>(); for (Set roles : rolesOrderedByOriginalStartupOrder) { if (roles == null) { // if some nodes were stopped, we want have a role for that ordinal @@ -1618,11 +1660,11 @@ public final class InternalTestCluster extends TestCluster { } assert nodesByRoles.values().stream().collect(Collectors.summingInt(List::size)) == 0; - // do two rounds to minimize pinging (mock zen pings pings with no delay and can create a lot of logs) for (NodeAndClient nodeAndClient : startUpOrder) { logger.info("resetting node [{}] ", nodeAndClient.name); // we already cleared data folders, before starting nodes up - nodeAndClient.recreateNodeOnRestart(callback, false, autoManageMinMasterNodes ? getMinMasterNodes(getMasterNodesCount()) : -1); + nodeAndClient.recreateNodeOnRestart(callback, false, autoManageMinMasterNodes ? getMinMasterNodes(getMasterNodesCount()) : -1, + () -> rebuildUnicastHostFiles(startUpOrder)); } startAndPublishNodesAndClients(startUpOrder); @@ -1741,9 +1783,9 @@ public final class InternalTestCluster extends TestCluster { } else { defaultMinMasterNodes = -1; } - List nodes = new ArrayList<>(); - for (Settings nodeSettings: settings) { - nodes.add(buildNode(nodeSettings, defaultMinMasterNodes)); + final List nodes = new ArrayList<>(); + for (Settings nodeSettings : settings) { + nodes.add(buildNode(nodeSettings, defaultMinMasterNodes, () -> rebuildUnicastHostFiles(nodes))); } startAndPublishNodesAndClients(nodes); if (autoManageMinMasterNodes) { diff --git a/test/framework/src/main/java/org/elasticsearch/test/discovery/MockUncasedHostProvider.java b/test/framework/src/main/java/org/elasticsearch/test/discovery/MockUncasedHostProvider.java deleted file mode 100644 index dc9304637cd..00000000000 --- a/test/framework/src/main/java/org/elasticsearch/test/discovery/MockUncasedHostProvider.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.elasticsearch.test.discovery; - -import org.elasticsearch.cluster.ClusterName; -import org.elasticsearch.cluster.node.DiscoveryNode; -import org.elasticsearch.common.Nullable; -import org.elasticsearch.common.transport.TransportAddress; -import org.elasticsearch.common.util.concurrent.ConcurrentCollections; -import org.elasticsearch.discovery.zen.UnicastHostsProvider; - -import java.io.Closeable; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -/** - * A {@link UnicastHostsProvider} implementation which returns results based on a static in-memory map. This allows running - * with nodes that only determine their transport address at runtime, which is the default behavior of - * {@link org.elasticsearch.test.InternalTestCluster} - */ -public final class MockUncasedHostProvider implements UnicastHostsProvider, Closeable { - - static final Map> activeNodesPerCluster = new HashMap<>(); - - - private final Supplier localNodeSupplier; - private final ClusterName clusterName; - - public MockUncasedHostProvider(Supplier localNodeSupplier, ClusterName clusterName) { - this.localNodeSupplier = localNodeSupplier; - this.clusterName = clusterName; - synchronized (activeNodesPerCluster) { - getActiveNodesForCurrentCluster().add(this); - } - } - - @Override - public List buildDynamicHosts(HostsResolver hostsResolver) { - final DiscoveryNode localNode = getNode(); - assert localNode != null; - synchronized (activeNodesPerCluster) { - Set activeNodes = getActiveNodesForCurrentCluster(); - return activeNodes.stream() - .map(MockUncasedHostProvider::getNode) - .filter(Objects::nonNull) - .filter(n -> localNode.equals(n) == false) - .map(DiscoveryNode::getAddress) - .collect(Collectors.toList()); - } - } - - @Nullable - private DiscoveryNode getNode() { - return localNodeSupplier.get(); - } - - private Set getActiveNodesForCurrentCluster() { - assert Thread.holdsLock(activeNodesPerCluster); - return activeNodesPerCluster.computeIfAbsent(clusterName, - clusterName -> ConcurrentCollections.newConcurrentSet()); - } - - @Override - public void close() { - synchronized (activeNodesPerCluster) { - boolean found = getActiveNodesForCurrentCluster().remove(this); - assert found; - } - } -} diff --git a/test/framework/src/main/java/org/elasticsearch/test/discovery/TestZenDiscovery.java b/test/framework/src/main/java/org/elasticsearch/test/discovery/TestZenDiscovery.java index 5387a659aa2..2c8305b4e12 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/discovery/TestZenDiscovery.java +++ b/test/framework/src/main/java/org/elasticsearch/test/discovery/TestZenDiscovery.java @@ -19,13 +19,10 @@ package org.elasticsearch.test.discovery; -import org.apache.lucene.util.SetOnce; -import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.routing.allocation.AllocationService; import org.elasticsearch.cluster.service.ClusterApplier; import org.elasticsearch.cluster.service.MasterService; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; -import org.elasticsearch.common.network.NetworkService; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; @@ -39,7 +36,6 @@ import org.elasticsearch.plugins.Plugin; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; -import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.Map; @@ -59,7 +55,6 @@ public class TestZenDiscovery extends ZenDiscovery { /** A plugin which installs mock discovery and configures it to be used. */ public static class TestPlugin extends Plugin implements DiscoveryPlugin { protected final Settings settings; - private final SetOnce unicastHostProvider = new SetOnce<>(); public TestPlugin(Settings settings) { this.settings = settings; } @@ -78,26 +73,6 @@ public class TestZenDiscovery extends ZenDiscovery { clusterApplier, clusterSettings, hostsProvider, allocationService)); } - @Override - public Map> getZenHostsProviders(TransportService transportService, - NetworkService networkService) { - final Supplier supplier; - if (USE_MOCK_PINGS.get(settings)) { - // we have to return something in order for the unicast host provider setting to resolve to something. It will never be used - supplier = () -> hostsResolver -> { - throw new UnsupportedOperationException(); - }; - } else { - supplier = () -> { - unicastHostProvider.set( - new MockUncasedHostProvider(transportService::getLocalNode, ClusterName.CLUSTER_NAME_SETTING.get(settings)) - ); - return unicastHostProvider.get(); - }; - } - return Collections.singletonMap("test-zen", supplier); - } - @Override public List> getSettings() { return Collections.singletonList(USE_MOCK_PINGS); @@ -107,18 +82,9 @@ public class TestZenDiscovery extends ZenDiscovery { public Settings additionalSettings() { return Settings.builder() .put(DiscoveryModule.DISCOVERY_TYPE_SETTING.getKey(), "test-zen") - .put(DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING.getKey(), "test-zen") .putList(DISCOVERY_ZEN_PING_UNICAST_HOSTS_SETTING.getKey()) .build(); } - - @Override - public void close() throws IOException { - super.close(); - if (unicastHostProvider.get() != null) { - unicastHostProvider.get().close(); - } - } } private TestZenDiscovery(Settings settings, ThreadPool threadPool, TransportService transportService, diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/license/LicensingTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/license/LicensingTests.java index a63dd94b639..ad1bb7be95c 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/license/LicensingTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/license/LicensingTests.java @@ -8,6 +8,7 @@ package org.elasticsearch.license; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.action.DocWriteResponse; import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; +import org.elasticsearch.action.admin.cluster.node.info.NodesInfoRequest; import org.elasticsearch.action.admin.cluster.node.stats.NodesStatsResponse; import org.elasticsearch.action.admin.cluster.stats.ClusterStatsIndices; import org.elasticsearch.action.admin.cluster.stats.ClusterStatsResponse; @@ -52,8 +53,11 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.elasticsearch.discovery.zen.SettingsBasedHostsProvider.DISCOVERY_ZEN_PING_UNICAST_HOSTS_SETTING; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoFailures; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.greaterThanOrEqualTo; @@ -278,6 +282,10 @@ public class LicensingTests extends SecurityIntegTestCase { enableLicensing(mode); ensureGreen(); + final List unicastHostsList = internalCluster().masterClient().admin().cluster().nodesInfo(new NodesInfoRequest()).get() + .getNodes().stream().map(n -> n.getTransport().getAddress().publishAddress().toString()).distinct() + .collect(Collectors.toList()); + Path home = createTempDir(); Path conf = home.resolve("config"); Files.createDirectories(conf); @@ -291,7 +299,8 @@ public class LicensingTests extends SecurityIntegTestCase { .put("path.home", home) .put(TestZenDiscovery.USE_MOCK_PINGS.getKey(), false) .put(DiscoveryModule.DISCOVERY_TYPE_SETTING.getKey(), "test-zen") - .put(DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING.getKey(), "test-zen") + .putList(DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING.getKey()) + .putList(DISCOVERY_ZEN_PING_UNICAST_HOSTS_SETTING.getKey(), unicastHostsList) .build(); Collection> mockPlugins = Arrays.asList(LocalStateSecurity.class, TestZenDiscovery.TestPlugin.class, MockHttpTransport.TestPlugin.class); From 6ca36bba15af11c30850a7401ec6e0f552296407 Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Thu, 13 Sep 2018 09:21:27 +0200 Subject: [PATCH 49/78] Fix field mapping updates with similarity (#33634) This change fixes a bug introduced in 6.3 that prevents fields with an explicit similarity to be updated. It also adds a test that checks this case for similarities but also for analyzers since they could suffer from the same problem. Closes #33611 --- .../index/mapper/MappedFieldType.java | 14 ++-------- .../index/similarity/SimilarityProvider.java | 26 +++++++++++++++++++ .../index/mapper/FieldTypeTestCase.java | 22 ++++++++++++++++ 3 files changed, 50 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java b/server/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java index 4a3fa852e7f..82a601de05e 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java @@ -111,17 +111,6 @@ public abstract class MappedFieldType extends FieldType { public boolean equals(Object o) { if (!super.equals(o)) return false; MappedFieldType fieldType = (MappedFieldType) o; - // check similarity first because we need to check the name, and it might be null - // TODO: SimilarityProvider should have equals? - if (similarity == null || fieldType.similarity == null) { - if (similarity != fieldType.similarity) { - return false; - } - } else { - if (Objects.equals(similarity.name(), fieldType.similarity.name()) == false) { - return false; - } - } return boost == fieldType.boost && docValues == fieldType.docValues && @@ -131,7 +120,8 @@ public abstract class MappedFieldType extends FieldType { Objects.equals(searchQuoteAnalyzer(), fieldType.searchQuoteAnalyzer()) && Objects.equals(eagerGlobalOrdinals, fieldType.eagerGlobalOrdinals) && Objects.equals(nullValue, fieldType.nullValue) && - Objects.equals(nullValueAsString, fieldType.nullValueAsString); + Objects.equals(nullValueAsString, fieldType.nullValueAsString) && + Objects.equals(similarity, fieldType.similarity); } @Override diff --git a/server/src/main/java/org/elasticsearch/index/similarity/SimilarityProvider.java b/server/src/main/java/org/elasticsearch/index/similarity/SimilarityProvider.java index fed15b30583..f5a870441d4 100644 --- a/server/src/main/java/org/elasticsearch/index/similarity/SimilarityProvider.java +++ b/server/src/main/java/org/elasticsearch/index/similarity/SimilarityProvider.java @@ -21,6 +21,8 @@ package org.elasticsearch.index.similarity; import org.apache.lucene.search.similarities.Similarity; +import java.util.Objects; + /** * Wrapper around a {@link Similarity} and its name. */ @@ -48,4 +50,28 @@ public final class SimilarityProvider { return similarity; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SimilarityProvider that = (SimilarityProvider) o; + /** + * We check name only because the similarity is + * re-created for each new instance and they don't implement equals. + * This is not entirely correct though but we only use equality checks + * for similarities inside the same index and names are unique in this case. + **/ + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + /** + * We use name only because the similarity is + * re-created for each new instance and they don't implement equals. + * This is not entirely correct though but we only use equality checks + * for similarities a single index and names are unique in this case. + **/ + return Objects.hash(name); + } } diff --git a/test/framework/src/main/java/org/elasticsearch/index/mapper/FieldTypeTestCase.java b/test/framework/src/main/java/org/elasticsearch/index/mapper/FieldTypeTestCase.java index 28767cb34d7..42eab104d6a 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/mapper/FieldTypeTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/index/mapper/FieldTypeTestCase.java @@ -89,6 +89,17 @@ public abstract class FieldTypeTestCase extends ESTestCase { other.setIndexAnalyzer(new NamedAnalyzer("foo", AnalyzerScope.INDEX, new StandardAnalyzer())); } }, + // check that we can update if the analyzer is unchanged + new Modifier("analyzer", true) { + @Override + public void modify(MappedFieldType ft) { + ft.setIndexAnalyzer(new NamedAnalyzer("foo", AnalyzerScope.INDEX, new StandardAnalyzer())); + } + @Override + public void normalizeOther(MappedFieldType other) { + other.setIndexAnalyzer(new NamedAnalyzer("foo", AnalyzerScope.INDEX, new StandardAnalyzer())); + } + }, new Modifier("search_analyzer", true) { @Override public void modify(MappedFieldType ft) { @@ -137,6 +148,17 @@ public abstract class FieldTypeTestCase extends ESTestCase { other.setSimilarity(new SimilarityProvider("bar", new BM25Similarity())); } }, + // check that we can update if the similarity is unchanged + new Modifier("similarity", true) { + @Override + public void modify(MappedFieldType ft) { + ft.setSimilarity(new SimilarityProvider("foo", new BM25Similarity())); + } + @Override + public void normalizeOther(MappedFieldType other) { + other.setSimilarity(new SimilarityProvider("foo", new BM25Similarity())); + } + }, new Modifier("eager_global_ordinals", true) { @Override public void modify(MappedFieldType ft) { From 7d3b99a9b7998997c54aa4678726e975d9395be7 Mon Sep 17 00:00:00 2001 From: Marios Trivyzas Date: Thu, 13 Sep 2018 10:28:05 +0200 Subject: [PATCH 50/78] SQL: Fix result column names for CAST (#33604) Previously, when an non-pruned cast (casting as a different data type) got applied on a table column in the `SELECT` clause, the name of the result column didn't contain the target data type of the cast, e.g.: SELECT CAST(MAX(salary) AS DOUBLE) FROM "test_emp" returned as column name: CAST(MAX(salary)) instead of: CAST(MAX(salary) AS DOUBLE) Closes #33571 * Added more tests for trivial casts that are pruned --- .../sql/expression/function/scalar/Cast.java | 9 ++++++- x-pack/qa/sql/src/main/resources/agg.csv-spec | 24 +++++++++++++++++++ x-pack/qa/sql/src/main/resources/agg.sql-spec | 18 ++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Cast.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Cast.java index 4d68ad57cf9..ae94b0b9f83 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Cast.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Cast.java @@ -111,4 +111,11 @@ public class Cast extends UnaryScalarFunction { public String toString() { return functionName() + "(" + field().toString() + " AS " + to().sqlName() + ")#" + id(); } -} \ No newline at end of file + + @Override + public String name() { + StringBuilder sb = new StringBuilder(super.name()); + sb.insert(sb.length() - 1, " AS " + to().sqlName()); + return sb.toString(); + } +} diff --git a/x-pack/qa/sql/src/main/resources/agg.csv-spec b/x-pack/qa/sql/src/main/resources/agg.csv-spec index 1d9592d963d..d274e5379c9 100644 --- a/x-pack/qa/sql/src/main/resources/agg.csv-spec +++ b/x-pack/qa/sql/src/main/resources/agg.csv-spec @@ -74,6 +74,30 @@ SELECT SUM(salary) FROM test_emp; 4824855 ; +aggregateWithCastPruned +SELECT CAST(SUM(salary) AS INTEGER) FROM test_emp; + + SUM(salary) +------------- +4824855 +; + +aggregateWithUpCast +SELECT CAST(SUM(salary) AS DOUBLE) FROM test_emp; + + CAST(SUM(salary) AS DOUBLE) +----------------------------- +4824855.0 +; + +aggregateWithCastNumericToString +SELECT CAST(AVG(salary) AS VARCHAR) FROM test_emp; + + CAST(AVG(salary) AS VARCHAR):s +-------------------------------- +48248.55 +; + kurtosisAndSkewnessNoGroup SELECT KURTOSIS(emp_no) k, SKEWNESS(salary) s FROM test_emp; diff --git a/x-pack/qa/sql/src/main/resources/agg.sql-spec b/x-pack/qa/sql/src/main/resources/agg.sql-spec index a86b8b65eef..e2213caa597 100644 --- a/x-pack/qa/sql/src/main/resources/agg.sql-spec +++ b/x-pack/qa/sql/src/main/resources/agg.sql-spec @@ -90,6 +90,10 @@ aggCountImplicit SELECT COUNT(*) AS count FROM test_emp; aggCountImplicitWithCast SELECT CAST(COUNT(*) AS INT) c FROM "test_emp"; +aggCountImplicitWithUpCast +SELECT CAST(COUNT(*) AS DOUBLE) c FROM "test_emp"; +aggCountImplicitWithPrunedCast +SELECT CAST(COUNT(*) AS BIGINT) c FROM "test_emp"; aggCountImplicitWithConstant SELECT COUNT(1) FROM "test_emp"; aggCountImplicitWithConstantAndFilter @@ -184,6 +188,10 @@ SELECT MIN(emp_no) AS min FROM test_emp; // end::min aggMinImplicitWithCast SELECT CAST(MIN(emp_no) AS SMALLINT) m FROM "test_emp"; +aggMinImplicitWithUpCast +SELECT CAST(MIN(emp_no) AS DOUBLE) m FROM "test_emp"; +aggMinImplicitWithPrunedCast +SELECT CAST(MIN(emp_no) AS INTEGER) m FROM "test_emp"; aggMin SELECT gender g, MIN(emp_no) m FROM "test_emp" GROUP BY gender ORDER BY gender; aggMinWithCast @@ -236,6 +244,10 @@ aggMaxImplicit SELECT MAX(salary) AS max FROM test_emp; aggMaxImplicitWithCast SELECT CAST(MAX(emp_no) AS SMALLINT) c FROM "test_emp"; +aggMaxImplicitWithUpCast +SELECT CAST(MAX(emp_no) AS DOUBLE) c FROM "test_emp"; +aggMaxImplicitWithPrunedCast +SELECT CAST(MAX(emp_no) AS INTEGER) c FROM "test_emp"; aggMax SELECT gender g, MAX(emp_no) m FROM "test_emp" GROUP BY gender ORDER BY gender; aggMaxWithCast @@ -268,6 +280,10 @@ SELECT gender g, MAX(emp_no) m FROM "test_emp" GROUP BY g HAVING m > 10 AND MAX( // SUM aggSumImplicitWithCast SELECT CAST(SUM(emp_no) AS BIGINT) s FROM "test_emp"; +aggSumImplicitWithUpCast +SELECT CAST(SUM(emp_no) AS DOUBLE) s FROM "test_emp"; +aggSumImplicitWithUpCast +SELECT CAST(SUM(emp_no) AS INTEGER) s FROM "test_emp"; aggSumWithCast SELECT gender g, CAST(SUM(emp_no) AS BIGINT) s FROM "test_emp" GROUP BY gender ORDER BY gender; aggSumWithCastAndCount @@ -298,6 +314,8 @@ SELECT gender g, CAST(SUM(emp_no) AS INT) s FROM "test_emp" GROUP BY g HAVING s // AVG aggAvgImplicitWithCast SELECT CAST(AVG(emp_no) AS FLOAT) a FROM "test_emp"; +aggAvgImplicitWithUpCast +SELECT CAST(AVG(emp_no) AS DOUBLE) a FROM "test_emp"; aggAvgWithCastToFloat SELECT gender g, CAST(AVG(emp_no) AS FLOAT) a FROM "test_emp" GROUP BY gender ORDER BY gender; // casting to an exact type - varchar, bigint, etc... will likely fail due to rounding error From a69ae6b89f0560b2dfc97e86eebd1268e945e2b9 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Thu, 13 Sep 2018 11:36:52 +0200 Subject: [PATCH 51/78] [CCR] Add metadata to keep track of the index uuid of the leader index in the follow index (#33367) The follow index api checks if the recorded uuid in the follow index matches with uuid of the leader index and fails otherwise. This validation will prevent a follow index from following an incompatible leader index. The create_and_follow api will automatically add this custom index metadata when it creates the follow index. Closes #31505 --- .../java/org/elasticsearch/xpack/ccr/Ccr.java | 1 + .../TransportCreateAndFollowIndexAction.java | 1 + .../action/TransportFollowIndexAction.java | 8 ++ .../xpack/ccr/ShardChangesIT.java | 83 ++++++++++--------- .../TransportFollowIndexActionTests.java | 55 ++++++++---- 5 files changed, 92 insertions(+), 56 deletions(-) diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java index 4e4caf8500f..eddb3570dee 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java @@ -87,6 +87,7 @@ public class Ccr extends Plugin implements ActionPlugin, PersistentTaskPlugin, E public static final String CCR_THREAD_POOL_NAME = "ccr"; public static final String CCR_CUSTOM_METADATA_KEY = "ccr"; public static final String CCR_CUSTOM_METADATA_LEADER_INDEX_SHARD_HISTORY_UUIDS = "leader_index_shard_history_uuids"; + public static final String CCR_CUSTOM_METADATA_LEADER_INDEX_UUID_KEY = "leader_index_uuid"; private final boolean enabled; private final Settings settings; diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportCreateAndFollowIndexAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportCreateAndFollowIndexAction.java index c6d1a7c36c5..e795a903729 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportCreateAndFollowIndexAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportCreateAndFollowIndexAction.java @@ -183,6 +183,7 @@ public final class TransportCreateAndFollowIndexAction // Adding the leader index uuid for each shard as custom metadata: Map metadata = new HashMap<>(); metadata.put(Ccr.CCR_CUSTOM_METADATA_LEADER_INDEX_SHARD_HISTORY_UUIDS, String.join(",", historyUUIDs)); + metadata.put(Ccr.CCR_CUSTOM_METADATA_LEADER_INDEX_UUID_KEY, leaderIndexMetaData.getIndexUUID()); imdBuilder.putCustom(Ccr.CCR_CUSTOM_METADATA_KEY, metadata); // Copy all settings, but overwrite a few settings. diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexAction.java index fff3f1618aa..9e1d2cc44ac 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexAction.java @@ -245,6 +245,14 @@ public class TransportFollowIndexAction extends HandledTransportAction { @@ -339,7 +335,7 @@ public class ShardChangesIT extends ESIntegTestCase { final FollowIndexAction.Request followRequest = new FollowIndexAction.Request("index1", "index2", randomIntBetween(32, 2048), randomIntBetween(2, 10), Long.MAX_VALUE, randomIntBetween(2, 10), FollowIndexAction.DEFAULT_MAX_WRITE_BUFFER_SIZE, TimeValue.timeValueMillis(500), TimeValue.timeValueMillis(10)); - client().execute(FollowIndexAction.INSTANCE, followRequest).get(); + client().execute(CreateAndFollowIndexAction.INSTANCE, new CreateAndFollowIndexAction.Request(followRequest)).get(); long maxNumDocsReplicated = Math.min(1000, randomLongBetween(followRequest.getMaxBatchOperationCount(), followRequest.getMaxBatchOperationCount() * 10)); @@ -416,34 +412,6 @@ public class ShardChangesIT extends ESIntegTestCase { expectThrows(IndexNotFoundException.class, () -> client().execute(FollowIndexAction.INSTANCE, followRequest3).actionGet()); } - @TestLogging("_root:DEBUG") - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/33379") - public void testValidateFollowingIndexSettings() throws Exception { - assertAcked(client().admin().indices().prepareCreate("test-leader") - .setSettings(Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true))); - // TODO: indexing should be optional but the current mapping logic requires for now. - client().prepareIndex("test-leader", "doc", "id").setSource("{\"f\": \"v\"}", XContentType.JSON).get(); - assertAcked(client().admin().indices().prepareCreate("test-follower").get()); - IllegalArgumentException followError = expectThrows(IllegalArgumentException.class, () -> client().execute( - FollowIndexAction.INSTANCE, createFollowRequest("test-leader", "test-follower")).actionGet()); - assertThat(followError.getMessage(), equalTo("the following index [test-follower] is not ready to follow;" + - " the setting [index.xpack.ccr.following_index] must be enabled.")); - // updating the `following_index` with an open index must not be allowed. - IllegalArgumentException updateError = expectThrows(IllegalArgumentException.class, () -> { - client().admin().indices().prepareUpdateSettings("test-follower") - .setSettings(Settings.builder().put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true)).get(); - }); - assertThat(updateError.getMessage(), containsString("Can't update non dynamic settings " + - "[[index.xpack.ccr.following_index]] for open indices [[test-follower/")); - assertAcked(client().admin().indices().prepareClose("test-follower")); - assertAcked(client().admin().indices().prepareUpdateSettings("test-follower") - .setSettings(Settings.builder().put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true))); - assertAcked(client().admin().indices().prepareOpen("test-follower")); - assertAcked(client().execute(FollowIndexAction.INSTANCE, - createFollowRequest("test-leader", "test-follower")).actionGet()); - unfollowIndex("test-follower"); - } - public void testFollowIndex_lowMaxTranslogBytes() throws Exception { final String leaderIndexSettings = getIndexSettings(1, between(0, 1), singletonMap(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true")); @@ -478,6 +446,37 @@ public class ShardChangesIT extends ESIntegTestCase { unfollowIndex("index2"); } + public void testDontFollowTheWrongIndex() throws Exception { + String leaderIndexSettings = getIndexSettings(1, 0, + Collections.singletonMap(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true")); + assertAcked(client().admin().indices().prepareCreate("index1").setSource(leaderIndexSettings, XContentType.JSON)); + ensureGreen("index1"); + assertAcked(client().admin().indices().prepareCreate("index3").setSource(leaderIndexSettings, XContentType.JSON)); + ensureGreen("index3"); + + FollowIndexAction.Request followRequest = new FollowIndexAction.Request("index1", "index2", 1024, 1, 1024L, + 1, 10240, TimeValue.timeValueMillis(500), TimeValue.timeValueMillis(10)); + CreateAndFollowIndexAction.Request createAndFollowRequest = new CreateAndFollowIndexAction.Request(followRequest); + client().execute(CreateAndFollowIndexAction.INSTANCE, createAndFollowRequest).get(); + + followRequest = new FollowIndexAction.Request("index3", "index4", 1024, 1, 1024L, + 1, 10240, TimeValue.timeValueMillis(500), TimeValue.timeValueMillis(10)); + createAndFollowRequest = new CreateAndFollowIndexAction.Request(followRequest); + client().execute(CreateAndFollowIndexAction.INSTANCE, createAndFollowRequest).get(); + unfollowIndex("index2", "index4"); + + FollowIndexAction.Request wrongRequest1 = new FollowIndexAction.Request("index1", "index4", 1024, 1, 1024L, + 1, 10240, TimeValue.timeValueMillis(500), TimeValue.timeValueMillis(10)); + Exception e = expectThrows(IllegalArgumentException.class, + () -> client().execute(FollowIndexAction.INSTANCE, wrongRequest1).actionGet()); + assertThat(e.getMessage(), containsString("follow index [index4] should reference")); + + FollowIndexAction.Request wrongRequest2 = new FollowIndexAction.Request("index3", "index2", 1024, 1, 1024L, + 1, 10240, TimeValue.timeValueMillis(500), TimeValue.timeValueMillis(10)); + e = expectThrows(IllegalArgumentException.class, () -> client().execute(FollowIndexAction.INSTANCE, wrongRequest2).actionGet()); + assertThat(e.getMessage(), containsString("follow index [index2] should reference")); + } + private CheckedRunnable assertTask(final int numberOfPrimaryShards, final Map numDocsPerShard) { return () -> { final ClusterState clusterState = client().admin().cluster().prepareState().get().getState(); @@ -514,10 +513,12 @@ public class ShardChangesIT extends ESIntegTestCase { }; } - private void unfollowIndex(String index) throws Exception { - final UnfollowIndexAction.Request unfollowRequest = new UnfollowIndexAction.Request(); - unfollowRequest.setFollowIndex(index); - client().execute(UnfollowIndexAction.INSTANCE, unfollowRequest).get(); + private void unfollowIndex(String... indices) throws Exception { + for (String index : indices) { + final UnfollowIndexAction.Request unfollowRequest = new UnfollowIndexAction.Request(); + unfollowRequest.setFollowIndex(index); + client().execute(UnfollowIndexAction.INSTANCE, unfollowRequest).get(); + } assertBusy(() -> { final ClusterState clusterState = client().admin().cluster().prepareState().get().getState(); final PersistentTasksCustomMetaData tasks = clusterState.getMetaData().custom(PersistentTasksCustomMetaData.TYPE); diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexActionTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexActionTests.java index d671bbd1875..f168bccc8ca 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexActionTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexActionTests.java @@ -20,6 +20,7 @@ import org.elasticsearch.xpack.ccr.ShardChangesIT; import org.elasticsearch.xpack.core.ccr.action.FollowIndexAction; import java.io.IOException; +import java.util.HashMap; import java.util.Map; import static java.util.Collections.emptyMap; @@ -29,10 +30,11 @@ import static org.hamcrest.Matchers.equalTo; public class TransportFollowIndexActionTests extends ESTestCase { - private static final Map CUSTOM_METADATA = - singletonMap(Ccr.CCR_CUSTOM_METADATA_LEADER_INDEX_SHARD_HISTORY_UUIDS, "uuid"); - public void testValidation() throws IOException { + final Map customMetaData = new HashMap<>(); + customMetaData.put(Ccr.CCR_CUSTOM_METADATA_LEADER_INDEX_SHARD_HISTORY_UUIDS, "uuid"); + customMetaData.put(Ccr.CCR_CUSTOM_METADATA_LEADER_INDEX_UUID_KEY, "_na_"); + FollowIndexAction.Request request = ShardChangesIT.createFollowRequest("index1", "index2"); String[] UUIDs = new String[]{"uuid"}; { @@ -47,12 +49,23 @@ public class TransportFollowIndexActionTests extends ESTestCase { () -> validate(request, leaderIMD, null, null, null)); assertThat(e.getMessage(), equalTo("follow index [index2] does not exist")); } + { + // should fail because the recorded leader index uuid is not equal to the leader actual index + IndexMetaData leaderIMD = createIMD("index1", 5, Settings.EMPTY, customMetaData); + IndexMetaData followIMD = createIMD("index2", 5, Settings.EMPTY, + singletonMap(Ccr.CCR_CUSTOM_METADATA_LEADER_INDEX_UUID_KEY, "another-value")); + Exception e = expectThrows(IllegalArgumentException.class, + () -> validate(request, leaderIMD, followIMD, UUIDs, null)); + assertThat(e.getMessage(), equalTo("follow index [index2] should reference [_na_] as leader index but " + + "instead reference [another-value] as leader index")); + } { // should fail because the recorded leader index history uuid is not equal to the leader actual index history uuid: IndexMetaData leaderIMD = createIMD("index1", 5, Settings.EMPTY, emptyMap()); - Map customMetaData = - singletonMap(Ccr.CCR_CUSTOM_METADATA_LEADER_INDEX_SHARD_HISTORY_UUIDS, "another-uuid"); - IndexMetaData followIMD = createIMD("index2", 5, Settings.EMPTY, customMetaData); + Map anotherCustomMetaData = new HashMap<>(); + anotherCustomMetaData.put(Ccr.CCR_CUSTOM_METADATA_LEADER_INDEX_UUID_KEY, "_na_"); + anotherCustomMetaData.put(Ccr.CCR_CUSTOM_METADATA_LEADER_INDEX_SHARD_HISTORY_UUIDS, "another-uuid"); + IndexMetaData followIMD = createIMD("index2", 5, Settings.EMPTY, anotherCustomMetaData); Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, UUIDs, null)); assertThat(e.getMessage(), equalTo("leader shard [index2][0] should reference [another-uuid] as history uuid but " + @@ -61,7 +74,7 @@ public class TransportFollowIndexActionTests extends ESTestCase { { // should fail because leader index does not have soft deletes enabled IndexMetaData leaderIMD = createIMD("index1", 5, Settings.EMPTY, emptyMap()); - IndexMetaData followIMD = createIMD("index2", 5, Settings.EMPTY, CUSTOM_METADATA); + IndexMetaData followIMD = createIMD("index2", 5, Settings.EMPTY, customMetaData); Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, UUIDs, null)); assertThat(e.getMessage(), equalTo("leader index [index1] does not have soft deletes enabled")); } @@ -69,7 +82,7 @@ public class TransportFollowIndexActionTests extends ESTestCase { // should fail because the number of primary shards between leader and follow index are not equal IndexMetaData leaderIMD = createIMD("index1", 5, Settings.builder() .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build(), emptyMap()); - IndexMetaData followIMD = createIMD("index2", 4, Settings.EMPTY, CUSTOM_METADATA); + IndexMetaData followIMD = createIMD("index2", 4, Settings.EMPTY, customMetaData); Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, UUIDs, null)); assertThat(e.getMessage(), equalTo("leader index primary shards [5] does not match with the number of shards of the follow index [4]")); @@ -79,16 +92,28 @@ public class TransportFollowIndexActionTests extends ESTestCase { IndexMetaData leaderIMD = createIMD("index1", State.CLOSE, "{}", 5, Settings.builder() .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build(), emptyMap()); IndexMetaData followIMD = createIMD("index2", State.OPEN, "{}", 5, Settings.builder() - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build(), CUSTOM_METADATA); + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build(), customMetaData); Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, UUIDs, null)); assertThat(e.getMessage(), equalTo("leader and follow index must be open")); } + { + // should fail, because index.xpack.ccr.following_index setting has not been enabled in leader index + IndexMetaData leaderIMD = createIMD("index1", 1, + Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build(), customMetaData); + IndexMetaData followIMD = createIMD("index2", 1, Settings.EMPTY, customMetaData); + MapperService mapperService = MapperTestUtils.newMapperService(xContentRegistry(), createTempDir(), Settings.EMPTY, "index2"); + mapperService.updateMapping(null, followIMD); + Exception e = expectThrows(IllegalArgumentException.class, + () -> validate(request, leaderIMD, followIMD, UUIDs, mapperService)); + assertThat(e.getMessage(), equalTo("the following index [index2] is not ready to follow; " + + "the setting [index.xpack.ccr.following_index] must be enabled.")); + } { // should fail, because leader has a field with the same name mapped as keyword and follower as text IndexMetaData leaderIMD = createIMD("index1", State.OPEN, "{\"properties\": {\"field\": {\"type\": \"keyword\"}}}", 5, Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build(), emptyMap()); IndexMetaData followIMD = createIMD("index2", State.OPEN, "{\"properties\": {\"field\": {\"type\": \"text\"}}}", 5, - Settings.builder().put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true).build(), CUSTOM_METADATA); + Settings.builder().put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true).build(), customMetaData); MapperService mapperService = MapperTestUtils.newMapperService(xContentRegistry(), createTempDir(), Settings.EMPTY, "index2"); mapperService.updateMapping(null, followIMD); Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, UUIDs, mapperService)); @@ -104,7 +129,7 @@ public class TransportFollowIndexActionTests extends ESTestCase { IndexMetaData followIMD = createIMD("index2", State.OPEN, mapping, 5, Settings.builder() .put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true) .put("index.analysis.analyzer.my_analyzer.type", "custom") - .put("index.analysis.analyzer.my_analyzer.tokenizer", "standard").build(), CUSTOM_METADATA); + .put("index.analysis.analyzer.my_analyzer.tokenizer", "standard").build(), customMetaData); Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, UUIDs, null)); assertThat(e.getMessage(), equalTo("the leader and follower index settings must be identical")); } @@ -114,7 +139,7 @@ public class TransportFollowIndexActionTests extends ESTestCase { Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build(), emptyMap()); Settings followingIndexSettings = randomBoolean() ? Settings.EMPTY : Settings.builder().put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), false).build(); - IndexMetaData followIMD = createIMD("index2", 5, followingIndexSettings, CUSTOM_METADATA); + IndexMetaData followIMD = createIMD("index2", 5, followingIndexSettings, customMetaData); MapperService mapperService = MapperTestUtils.newMapperService(xContentRegistry(), createTempDir(), followingIndexSettings, "index2"); mapperService.updateMapping(null, followIMD); @@ -128,7 +153,7 @@ public class TransportFollowIndexActionTests extends ESTestCase { IndexMetaData leaderIMD = createIMD("index1", 5, Settings.builder() .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build(), emptyMap()); IndexMetaData followIMD = createIMD("index2", 5, Settings.builder() - .put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true).build(), CUSTOM_METADATA); + .put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true).build(), customMetaData); MapperService mapperService = MapperTestUtils.newMapperService(xContentRegistry(), createTempDir(), Settings.EMPTY, "index2"); mapperService.updateMapping(null, followIMD); validate(request, leaderIMD, followIMD, UUIDs, mapperService); @@ -143,7 +168,7 @@ public class TransportFollowIndexActionTests extends ESTestCase { IndexMetaData followIMD = createIMD("index2", State.OPEN, mapping, 5, Settings.builder() .put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true) .put("index.analysis.analyzer.my_analyzer.type", "custom") - .put("index.analysis.analyzer.my_analyzer.tokenizer", "standard").build(), CUSTOM_METADATA); + .put("index.analysis.analyzer.my_analyzer.tokenizer", "standard").build(), customMetaData); MapperService mapperService = MapperTestUtils.newMapperService(xContentRegistry(), createTempDir(), followIMD.getSettings(), "index2"); mapperService.updateMapping(null, followIMD); @@ -161,7 +186,7 @@ public class TransportFollowIndexActionTests extends ESTestCase { .put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true) .put(IndexSettings.INDEX_REFRESH_INTERVAL_SETTING.getKey(), "10s") .put("index.analysis.analyzer.my_analyzer.type", "custom") - .put("index.analysis.analyzer.my_analyzer.tokenizer", "standard").build(), CUSTOM_METADATA); + .put("index.analysis.analyzer.my_analyzer.tokenizer", "standard").build(), customMetaData); MapperService mapperService = MapperTestUtils.newMapperService(xContentRegistry(), createTempDir(), followIMD.getSettings(), "index2"); mapperService.updateMapping(null, followIMD); From 6dfe54c8381e2b9046de836fb07fabaa03a02452 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Thu, 13 Sep 2018 06:35:36 -0400 Subject: [PATCH 52/78] Use serializable exception in GCP listeners (#33657) We used TimeoutException here but that's not serializable. This commit switches to a serializable exception so that we can test for the exception type on the remote side. --- .../index/shard/GlobalCheckpointListeners.java | 13 +++++++------ .../index/shard/GlobalCheckpointListenersTests.java | 9 +++++---- .../org/elasticsearch/index/shard/IndexShardIT.java | 4 ++-- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java b/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java index 224d5be17e1..e738ebac160 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java +++ b/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java @@ -21,6 +21,7 @@ package org.elasticsearch.index.shard; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.ElasticsearchTimeoutException; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.FutureUtils; @@ -33,7 +34,6 @@ import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import static org.elasticsearch.index.seqno.SequenceNumbers.NO_OPS_PERFORMED; import static org.elasticsearch.index.seqno.SequenceNumbers.UNASSIGNED_SEQ_NO; @@ -53,7 +53,8 @@ public class GlobalCheckpointListeners implements Closeable { * Callback when the global checkpoint is updated or the shard is closed. If the shard is closed, the value of the global checkpoint * will be set to {@link org.elasticsearch.index.seqno.SequenceNumbers#UNASSIGNED_SEQ_NO} and the exception will be non-null and an * instance of {@link IndexShardClosedException }. If the listener timed out waiting for notification then the exception will be - * non-null and an instance of {@link TimeoutException}. If the global checkpoint is updated, the exception will be null. + * non-null and an instance of {@link ElasticsearchTimeoutException}. If the global checkpoint is updated, the exception will be + * null. * * @param globalCheckpoint the updated global checkpoint * @param e if non-null, the shard is closed or the listener timed out @@ -96,8 +97,8 @@ public class GlobalCheckpointListeners implements Closeable { * shard is closed then the listener will be asynchronously notified on the executor used to construct this collection of global * checkpoint listeners. The listener will only be notified of at most one event, either the global checkpoint is updated or the shard * is closed. A listener must re-register after one of these events to receive subsequent events. Callers may add a timeout to be - * notified after if the timeout elapses. In this case, the listener will be notified with a {@link TimeoutException}. Passing null for - * the timeout means no timeout will be associated to the listener. + * notified after if the timeout elapses. In this case, the listener will be notified with a {@link ElasticsearchTimeoutException}. + * Passing null for the timeout means no timeout will be associated to the listener. * * @param currentGlobalCheckpoint the current global checkpoint known to the listener * @param listener the listener @@ -140,7 +141,7 @@ public class GlobalCheckpointListeners implements Closeable { removed = listeners != null && listeners.remove(listener) != null; } if (removed) { - final TimeoutException e = new TimeoutException(timeout.getStringRep()); + final ElasticsearchTimeoutException e = new ElasticsearchTimeoutException(timeout.getStringRep()); logger.trace("global checkpoint listener timed out", e); executor.execute(() -> notifyListener(listener, UNASSIGNED_SEQ_NO, e)); } @@ -225,7 +226,7 @@ public class GlobalCheckpointListeners implements Closeable { } else if (e instanceof IndexShardClosedException) { logger.warn("error notifying global checkpoint listener of closed shard", caught); } else { - assert e instanceof TimeoutException : e; + assert e instanceof ElasticsearchTimeoutException : e; logger.warn("error notifying global checkpoint listener of timeout", caught); } } diff --git a/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java b/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java index e5e2453682f..8a1070d56d5 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java @@ -21,6 +21,7 @@ package org.elasticsearch.index.shard; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.ElasticsearchTimeoutException; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.EsExecutors; @@ -42,7 +43,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -512,10 +512,11 @@ public class GlobalCheckpointListenersTests extends ESTestCase { try { notified.set(true); assertThat(g, equalTo(UNASSIGNED_SEQ_NO)); - assertThat(e, instanceOf(TimeoutException.class)); + assertThat(e, instanceOf(ElasticsearchTimeoutException.class)); assertThat(e, hasToString(containsString(timeout.getStringRep()))); final ArgumentCaptor message = ArgumentCaptor.forClass(String.class); - final ArgumentCaptor t = ArgumentCaptor.forClass(TimeoutException.class); + final ArgumentCaptor t = + ArgumentCaptor.forClass(ElasticsearchTimeoutException.class); verify(mockLogger).trace(message.capture(), t.capture()); assertThat(message.getValue(), equalTo("global checkpoint listener timed out")); assertThat(t.getValue(), hasToString(containsString(timeout.getStringRep()))); @@ -547,7 +548,7 @@ public class GlobalCheckpointListenersTests extends ESTestCase { try { notified.set(true); assertThat(g, equalTo(UNASSIGNED_SEQ_NO)); - assertThat(e, instanceOf(TimeoutException.class)); + assertThat(e, instanceOf(ElasticsearchTimeoutException.class)); } finally { latch.countDown(); } diff --git a/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java b/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java index 8fe1daefe6d..715860e6ffa 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java +++ b/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java @@ -19,6 +19,7 @@ package org.elasticsearch.index.shard; import org.apache.lucene.store.LockObtainFailedException; +import org.elasticsearch.ElasticsearchTimeoutException; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; @@ -89,7 +90,6 @@ import java.util.Locale; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; -import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -798,7 +798,7 @@ public class IndexShardIT extends ESSingleNodeTestCase { notified.set(true); assertThat(g, equalTo(UNASSIGNED_SEQ_NO)); assertNotNull(e); - assertThat(e, instanceOf(TimeoutException.class)); + assertThat(e, instanceOf(ElasticsearchTimeoutException.class)); assertThat(e.getMessage(), equalTo(timeout.getStringRep())); } finally { latch.countDown(); From d806a0e59d1cf0730c5f089b551385f667bad26e Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Thu, 13 Sep 2018 07:00:40 -0400 Subject: [PATCH 53/78] Fix race in global checkpoint listeners test This race can occur if the latch from the listener notifies the test thread and the test thread races ahead before the scheduler thread has a chance to emit the log message. This commit fixes this test by not counting down the latch until after the log message we are going to assert on has been emitted. --- .../shard/GlobalCheckpointListenersTests.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java b/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java index 8a1070d56d5..4a6383c50d2 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java @@ -49,10 +49,13 @@ import java.util.concurrent.atomic.AtomicLong; import static org.elasticsearch.index.seqno.SequenceNumbers.NO_OPS_PERFORMED; import static org.elasticsearch.index.seqno.SequenceNumbers.UNASSIGNED_SEQ_NO; +import static org.hamcrest.Matchers.any; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasToString; import static org.hamcrest.Matchers.instanceOf; +import static org.mockito.Matchers.argThat; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; @@ -561,19 +564,19 @@ public class GlobalCheckpointListenersTests extends ESTestCase { } public void testFailingListenerAfterTimeout() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); final Logger mockLogger = mock(Logger.class); + doAnswer(invocationOnMock -> { + latch.countDown(); + return null; + }).when(mockLogger).warn(argThat(any(String.class)), argThat(any(RuntimeException.class))); final GlobalCheckpointListeners globalCheckpointListeners = new GlobalCheckpointListeners(shardId, Runnable::run, scheduler, mockLogger); - final CountDownLatch latch = new CountDownLatch(1); final TimeValue timeout = TimeValue.timeValueMillis(randomIntBetween(1, 50)); globalCheckpointListeners.add( NO_OPS_PERFORMED, (g, e) -> { - try { - throw new RuntimeException("failure"); - } finally { - latch.countDown(); - } + throw new RuntimeException("failure"); }, timeout); latch.await(); From a192785fc84383aac8837a8467cc302dc0523ccf Mon Sep 17 00:00:00 2001 From: Costin Leau Date: Sat, 8 Sep 2018 16:23:43 +0300 Subject: [PATCH 54/78] DOC: Add SQL section on client applications Add setup instructions for a number of GUI SQL applications --- .../sql/client-apps/dbeaver-1-new-conn.png | Bin 0 -> 25260 bytes .../sql/client-apps/dbeaver-2-conn-es.png | Bin 0 -> 23146 bytes .../sql/client-apps/dbeaver-3-conn-props.png | Bin 0 -> 20325 bytes .../sql/client-apps/dbeaver-4-driver-ver.png | Bin 0 -> 29626 bytes .../sql/client-apps/dbeaver-5-test-conn.png | Bin 0 -> 28780 bytes .../images/sql/client-apps/dbeaver-6-data.png | Bin 0 -> 87596 bytes .../client-apps/dbvis-1-driver-manager.png | Bin 0 -> 60954 bytes .../images/sql/client-apps/dbvis-2-driver.png | Bin 0 -> 57074 bytes .../sql/client-apps/dbvis-3-new-conn.png | Bin 0 -> 55342 bytes .../sql/client-apps/dbvis-4-conn-props.png | Bin 0 -> 57094 bytes .../images/sql/client-apps/dbvis-5-data.png | Bin 0 -> 99284 bytes .../client-apps/squirell-1-view-drivers.png | Bin 0 -> 22210 bytes .../sql/client-apps/squirell-2-new-driver.png | Bin 0 -> 27109 bytes .../sql/client-apps/squirell-3-add-driver.png | Bin 0 -> 17121 bytes .../client-apps/squirell-4-driver-list.png | Bin 0 -> 29004 bytes .../sql/client-apps/squirell-5-add-alias.png | Bin 0 -> 22199 bytes .../client-apps/squirell-6-alias-props.png | Bin 0 -> 13045 bytes .../sql/client-apps/squirell-7-data.png | Bin 0 -> 44964 bytes .../workbench-1-manage-drivers.png | Bin 0 -> 16439 bytes .../client-apps/workbench-2-add-driver.png | Bin 0 -> 26008 bytes .../client-apps/workbench-3-connection.png | Bin 0 -> 35138 bytes .../sql/client-apps/workbench-4-data.png | Bin 0 -> 75863 bytes .../endpoints/client-apps/dbeaver.asciidoc | 57 ++++++++++++++++++ .../sql/endpoints/client-apps/dbvis.asciidoc | 42 +++++++++++++ .../sql/endpoints/client-apps/index.asciidoc | 21 +++++++ .../endpoints/client-apps/squirrel.asciidoc | 50 +++++++++++++++ .../endpoints/client-apps/workbench.asciidoc | 40 ++++++++++++ docs/reference/sql/endpoints/index.asciidoc | 1 + docs/reference/sql/endpoints/jdbc.asciidoc | 12 +++- docs/reference/sql/index.asciidoc | 2 + .../language/syntax/describe-table.asciidoc | 7 ++- 31 files changed, 228 insertions(+), 4 deletions(-) create mode 100644 docs/reference/images/sql/client-apps/dbeaver-1-new-conn.png create mode 100644 docs/reference/images/sql/client-apps/dbeaver-2-conn-es.png create mode 100644 docs/reference/images/sql/client-apps/dbeaver-3-conn-props.png create mode 100644 docs/reference/images/sql/client-apps/dbeaver-4-driver-ver.png create mode 100644 docs/reference/images/sql/client-apps/dbeaver-5-test-conn.png create mode 100644 docs/reference/images/sql/client-apps/dbeaver-6-data.png create mode 100644 docs/reference/images/sql/client-apps/dbvis-1-driver-manager.png create mode 100644 docs/reference/images/sql/client-apps/dbvis-2-driver.png create mode 100644 docs/reference/images/sql/client-apps/dbvis-3-new-conn.png create mode 100644 docs/reference/images/sql/client-apps/dbvis-4-conn-props.png create mode 100644 docs/reference/images/sql/client-apps/dbvis-5-data.png create mode 100644 docs/reference/images/sql/client-apps/squirell-1-view-drivers.png create mode 100644 docs/reference/images/sql/client-apps/squirell-2-new-driver.png create mode 100644 docs/reference/images/sql/client-apps/squirell-3-add-driver.png create mode 100644 docs/reference/images/sql/client-apps/squirell-4-driver-list.png create mode 100644 docs/reference/images/sql/client-apps/squirell-5-add-alias.png create mode 100644 docs/reference/images/sql/client-apps/squirell-6-alias-props.png create mode 100644 docs/reference/images/sql/client-apps/squirell-7-data.png create mode 100644 docs/reference/images/sql/client-apps/workbench-1-manage-drivers.png create mode 100644 docs/reference/images/sql/client-apps/workbench-2-add-driver.png create mode 100644 docs/reference/images/sql/client-apps/workbench-3-connection.png create mode 100644 docs/reference/images/sql/client-apps/workbench-4-data.png create mode 100644 docs/reference/sql/endpoints/client-apps/dbeaver.asciidoc create mode 100644 docs/reference/sql/endpoints/client-apps/dbvis.asciidoc create mode 100644 docs/reference/sql/endpoints/client-apps/index.asciidoc create mode 100644 docs/reference/sql/endpoints/client-apps/squirrel.asciidoc create mode 100644 docs/reference/sql/endpoints/client-apps/workbench.asciidoc diff --git a/docs/reference/images/sql/client-apps/dbeaver-1-new-conn.png b/docs/reference/images/sql/client-apps/dbeaver-1-new-conn.png new file mode 100644 index 0000000000000000000000000000000000000000..2307f0393266361aa9ba3772126145be29e8cb84 GIT binary patch literal 25260 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 L;ZT9K*R_8D#VK=l literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/dbeaver-2-conn-es.png b/docs/reference/images/sql/client-apps/dbeaver-2-conn-es.png new file mode 100644 index 0000000000000000000000000000000000000000..1ca209a57e5553c35c1648825bada8d25daa389e GIT binary patch literal 23146 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 QWue2(*%S8S^p({A0Y25mLI3~& literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/dbeaver-3-conn-props.png b/docs/reference/images/sql/client-apps/dbeaver-3-conn-props.png new file mode 100644 index 0000000000000000000000000000000000000000..7561e94bdd9914399befe0c892a9a2f9268e4cb8 GIT binary patch literal 20325 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 literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/dbeaver-5-test-conn.png b/docs/reference/images/sql/client-apps/dbeaver-5-test-conn.png new file mode 100644 index 0000000000000000000000000000000000000000..70f2a1dd4dc2f33e0e251323cd5c7b22d4279cd5 GIT binary patch literal 28780 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 yuW96Pe&h=0_miO#|Y7$fgL_kEO1O!BSjSzYX zDor31X^}1FXrL6bjGV{w(=UYa%SXj#A*!CTd zF~74u)dl*pu$*c+_&e0$Rq%*~g^{aw=axmF!)g<2t!M{uD%7Jzh+Xfq_F+~7xhrC) zqHn!o-#Ye!OZ@r6FfjVT@f&vTzOmXY;G5s9+}YfHnx2=bSO30SaV1X4rYz|~H>Tnd zkm~tl=R&NXQZWRi#Mk4ZXiK9OujA=Olwv$yvs)v-kE+?{mDf;TRG(Ca4>lp+2q-ZL zKEo96;Pout>6hqvzQ4b)uoQeky*v49uI=CN_xL&`DNFS^3AMW6%8f_=Is8ReI7mC} zw}icF()+@%(r+ye&5r+jMa;Q(Dp%};B!Y~zMf5J5S&y20QFngV|KHOhQ7fGGj3tUe zXV}>v@6K}VUuk4%eA@Vnk=YXPZyOh-GS1(E@gX|v5_P!ujaSczs+{Rdd1@pj`$0ku zg$+<`Joiu2o5fGzLgbgiV95(M-$=QMK3p4Ete&073f}|sK!f2q|Kb9y?J+2!HW6a zy7=4oMrxnnTI&~vEmkuX=okJ+$C++V>&oZzMs~a2W`nOc^9V`k))M7A2G7~V+>N=^*uGe{E<`V z7e1nXxN-#P1Q+7!Oe7cG=v&;Q#>4+~bF)|u5!WabbaN~0H(Te#yK<$xB=sCB-a961 zK4bW-N<}-?%&8NbDqdmn*R|8%PtdiSD|YA^n1Yw{Rk@sbbCKvri}tx>`Mx84k0SymeDmZ=XkA^Y#&M^ z9Vq6X*G7DxPOyy|d+GMe>K}tZ!7rIz%=J`H%q6EUSIr82d?D@7$op^OtMN!+uy1lk z|A4jmG6#r0XDT0|>;hT-*oWm4F%Z8*HZ)Fkkfl4O0RO#0QvqOr5*oyrEvqCgeZ9)4 z0+V&JV2~t!8);97#90;efH{z84-CGuFCxb@5P!d5k041Hkim4;yovK9=!So0uMfbD z-J>6CrIDDM@FS#mJKIF-B;enh{6z->2~~KGIQJT(=9baB1-h<`;w4s;t0;t0%gk`C1jj@1wplQ>_H#YpwaHne3)m$1b}SuMYz0lfY$5@DHrnB^C6#WP3Yyi_krg z8~|DdpdUS|@fc46j#z~FuUS~b)}5d|iOI!kp49rW?IAn=JB6|QDzEqcT`XP)1ppPz z$`F3u>?);^p6H;o02$%6k;Y+Dtz%-5O4EtCF+JqR%dOjiyB%;yl7sLy1N0a3zp#N zK_IcHTps=Vz5yhOE)QYuFeOM*C}<=FsQR2vtzbpz?vmdj?JFKdA>cok^4w5XtqD&{cJENrQ9A1f%U-#4q;;1aS7A+K6>#0lZfLx}*FFdKy@i$%EQ=o~pC zrwO*{JO^ZJ$YkB!56>gnUBe}e>P(x~pc8NNJZ1rJ>OFjpq~*XL4$l7ccM$xg9pW&c_ytn5MgZGjtGAL{qoS4eJc+jo-gpm-93H1=1~t{v6+ z!?N_DX6xpSqR`^e${7iP#hFMUnR(~8JyQ3TADP+Faz3-=m4bFxY-2qh+$j7}W!R)l zL0Nb;ybV`kfpGs>@R+kvLT6iU$a^mw>8#xO_Wdk#srQaj&Riun0fUEA1m8tCD0x1D zEPIzRKaT$X(0P~f;9KZh>}qei+VBzbM5z&8sEHI%r6-zx9icw158ZRt%BB;MC@Y~L zMiB6oS(=I$Q>4FA)o7_6n|>qC5PLxGoG8zO*Wjg!{JK4F$SG0qDxYrXRN)?CvTn=# zvG!+-rD=#R#0uA`k@@f`|F3G4*R(57aZH9k>vq`t1?Y3kjN{)s*6(nsQSMV?!w-8e z(r^wJfR_PiwzCfDH$%OW8n4ifCs2#ZA3-|P{M{iEU>uhI@)3knKs86%YvNt4{TwbHlHtBivsm%=)}5|D>l%KP zau-Y#F~tyi{M-I0sX0&q?RS{y9MlNe`SSe8SPF)r5n9Ah*`h^doR32oFm9fU%D~s} zU3dYp(z5r%mdHQ9P>n2~%eP1o?7nhixl}yEGRXOO%!MY`3h}rz5z3_v9uvNYA=M5+ zU3ZYcn*EW;)A944@t1O0NgRI!M`tR_|IMRgc#(dT)5>qQbM#`NKa~WONB4NTJogelnbx8Mi5BHU7UBt^R1K!Qc1u8!wQO>1E)p=2(DTJwl#FC55h>Ar;0MJqdI5 z+iGPcWg-^Dhi-Vmn(8-G+wkQ&f&(UA;-L&_O5`WqfN6MugqC*y`|DyxG2I+qx$;=U zizAT?bdv%@VYGfnJ#c*nfM$);*hwK}n-EkN-Mg`T^i6_Ri6Cs_mOSSh<$~2uy78Iy zQQpVQ!R$%T(_|$%6--7}N-t14Ke=C_1?i-YUO8n#Pzq}Q#k!C-vbUu z!so|(t5!r|zkXF4)CwK;kQn-Br}Tp&2C!!XCcGmN@IX)6+^2Y2EMvdhb;i#zkY$N) zAd>TMVV_|XDj`Yz(_u~cZoJ8qd=?!?Xw;*9B{TwQ%R)`s@U3ifCm4&WINYf_ll^ZW z#6syaO#`w}^ynv*Fp_^GgD;Io{(})7EchXSKl3=dz<(h~ z#GB{J=+j4U{qw%U3gLcn>94=ve5zluxBqiaTRa8$&xse8T$YXk*#5ofC`Wq4Pi${d zXSw_}Sg)JR2Xoi;+oaTT+|@C|xs)}}Vi>)1boH&M)$wBYNi(ZA1D$FGAtqg;k9&+O zsxu8;#|=nJswamW*T3ODgITQ@?eE1KWO#|b@R9p<*kHo|BMp4VDJ8Y#omf-jdaJ8u zbDDfF>#1F*&8xqzs{IjZ!CKoBvgmv0Tw0B+LrquG0x=bLN9q+dPcYL~b_uy>up@ge z)~04CjB*tJmJc{S@oq+f-NP`*se3gVH>Y`SJ|kf#s0og*EF(&REn>H;9QlLl#Z&=1 zVWt6~3r=0iAK@nsX4~I*5>j36MBHgxu^O#_K@yH_xXD=D^=~Q(NeqU0+U(mx*Q$a| zg+Ky!32AP|_Pc!M5h+`(NuxZd4)|65>-jYrwW_5n>e2Y`WyciC5_rTM>%Pvzr!D@k zaf-qrNs(&a72<+=^jvWe>HGt`a$=(x@O@IZHY3gFiiKv-$-=8AG@2wcrhd*3n(UMi ze;ghSN=@UJ%J-b2=t*LAJoMiIo6E>0=&_# z)rBQupn&n#o@!acGGvL=2uYrY*!a~Xafo$KAMduUFs;kffOq!YCF#dh720~!lq{_r&NwKejT-Vif&!CF9&7#4h9Uph-UQ&v zAUy`Agu9bL91Ox2l=!*?US&tD+8&3r>5-q2(C%u>KjLJ;1JbrU(sm8yL3o=8lwQq|^C}fbXnCT~w zVjs^|e7@}YBERp`V@f)ydD(0H&X#H|$&s3cI7JdQ$nWu^laA#J@fA+c3nwb{Jh}&y zh~<LjMu(LohadAKEZcac0SKVHUI8bV$sG;d149> zd6SiemVf0NSV{plP4*D1!h1ndB?EUqqtwkP^=G$!u-y(WiVq(g6hxmM?sx!e(oHwy zE`uFfP8u{9^1^r`2h{6FcF>)Nwfe5vf6ogF>E*IF&frP55W-B~oKiLQPQajJw1X$q z%cIqrWlx2x)M>8yJyA<_YY!-^FS_pRu62B_oIVfzD>mM6$m;7#iO6HP)X+2{Hyt-z z7_j`HAQKKp4g=&X^AMF_4Ox*gzt}qt_Cje@27Ze5~2pTDV1Z@<7>Bkx>px2gfxXy z)o58o+krn;GZaforOZc%S8gP(`>trmlsH&THI`2L2kbP_K71cfj@p)2DdWnIpV;EN zG2=9zv?5Iw`D5s_bA*-A{f)Y^iks(h2wgn8lXB-zD`K~hPJfK)JtH}^sr0h=-QGbF z<_@C&;}SkvFb{|STuJ6rPYl20qIv9Z2Vi>b|8NV6{g9ne=gfay5Q{#0B@(`XbpGRz zm>c8=`_6e-iKW(069r*|Uv#@;WKLlXA$@!pW{}7b;?!qQ7%SSAhLnVyG zEbRb`WS?SQrNImro&~>k414tTkGoTlLmw8}`>rz-FQU^5W@&v&e0LWwq)9;>Tpc5i z9w@&b!Sr6t%fGWXBGco?Rk}59kU&CA$m1ld`+b35J^^Q~P*dH??gIdT%hnD-xtbU_ z)nA@gaBIzb5{ip5RkQbQJC9!hG3v*fo9U4}*(_BiQf3r~Uw?+qc>rj5GAKss@j{r# zMa#mM4e~K~Z zY0G=ti(%HZ;X9p4&?4s=46(5x`d^##T$_^x5o_zc2B#WN3FoN8O!ud4RPzP(**~Yn zy?h_px=uR*-LIKa&+n;9i))RT?7^BRK9R$nV)xK@5Bb{P^|oBzqcw|@bt&cfx14Pn z3&bkI;xUBt;`NW~7^>%FD2Tq5JX~5cRkU8WDCpsiYTB_8peLeWch|R2p%VLmT1SZXibDevkKs{ze5mL8gg4u?I1DGyb&_9!2lYF5d z8}(`rkgG0ZoMci!B#eHFpVeFK!n&t71_U|;l?7{Ll(9)nJS-Dn|+ zOg}WcWzc5u;BAN3;9uUX5^s*it&Fd~_gpytqIVGG+jbZrsa>Rieq>Xq)%j(8vZ2Ng zHs6)#`s34GN`$Py0Rs||Xq5s~;1ynySg6RK^ZI3km1>&08y!_#R)38Xz}6lq82tS; ze|Lk|_|=gP(;9~lBFQi*mC8tEjmOB*?9kmb{4SEAuTisv4tii;0JDT7Wt8c66n(6; zjPS)jFP+^yT@FzeBY36)FE2#BPRno5%Jj2ID%$p&xVZp&4Sq^2>Kgwtw^yx}n-&!Q zk(lD!=1vYnAV-l=@=D6`>dlJACy}F7Za6$5c;o8{qFk^Ub$f6}eP_trWLbRk0I!}u zb+GP$BVLeV!$Zhu!h>2f1}OZ!!>1@FzcNlNcWOH+Z~rPK7`~Lz<21%ArQHw|7EJGH zE-;c_+>kcVUv~OfQPqPT$-*~G`5EcX$ZY3i&RQWyf9@|t8+TiaYhc}#`~-c=Qq~;g zWVKkT-1Vg&$YqIfOPQK?znr@aaCIs%-e14X6f@1UUT^Yg3&#j&?%L0CZL$E;Mljjo zu9$Xo{sDb9_U%jB=r!}33nHxn3ffn5lUU9t67tcFf-)AUW6`)`S=3n8j(`hY z;42SeX8K%&e|LLpKmFZfNSqzcUEbjF2Ts5^>!c#uvR_F!yuoh}o9gx>)@^4JJo!el zEh^Wz-ngek!ue`O<(h#=IQ7k5bwg~(l;=hUIb-GSYb~Jz@m-(W$W=)(K(EaqPkW?# z*oNZb)VE%pUk9L>%)-dbwv7U3Ks=@YQrp4yScrbEg0_6Sx3Nj#&ot!^7*`?k9-qW+ z1+U%sY&}^;y07FvDOg;~>Kku->!tvK`@NPH&yU!AXnIQKRNClc*Dm$= zVH219p^E(!w+B6%G_89oo#%+b3c1cRTmUh9valY+zb`|>l|~`e6P9qVpG;pQB7tKn zw4N4G)nx;w_#Z))BUt)?Qr!6FS#$?ko4o=#Pog|WH9Fv;(Ts;0L{F@HXFq-2Epi0= z?rZY5;#cx$Xj%5@Kl^|@8IV~|o(d@qFS=uiSog*VsV=Om#ICd#8>DYrdTY<0TnPt4{=!q-_xWs2_}aO!|6C9zR5u4^uS(D{AfRCCb(NfdNcWN4EJpE@pE!FP+G| z8q+7RZ7_Y#iu7otzfzXz6`mpFHz3t`HT2O7r<18FqY-&;Sf4mSmP2%5_h+^?S~#PZ zZ@2lh_j~HGuZ3DgdfD6Pa~(;9 zVz8@atylz=)O=Nt?S~P-7CJe=+&L<_*R;sjr#S_D$!YD-x#8Tw?GHZ{BHuvOI5^1y zD`EpdMVfMj+A(|H9lEHV5~{2G8jsdd%NDFFQ8f!v0sD(`ztBM=T~rT}BuBey4_G+= zh;sinCI0Hk@z0K7r=pe6M`M_-@!72d2QwT1iHaKK7<vcJadO zE!+#G4p__9$k^_9Sle<1K9GtOg6j%U`xp+&l>$__O+64(U1a_i9mA z^|e9aUCU}CYl@z=gut4Q`+Ie-5DfSf9pS7DPfeY$y%-}QU<@#@u$OHAGO*)OGnvQ8 zcsk5`;IJdYnK~eUQFk{Tg$1_9ed`R|eMNQ zo^RRREhk~S>>lRT1hs;liyI69yCM^Fz{>27bbcq74UYsU>|EwjJL&>)<@=?!xtFt) zH^ZX@#WH?wgR24p0F;H#E~=q^Q`=EJ&h#RtM}B5tv*_uxf6dII(zwxLq2~{CfEpmH zy@c@n>^76P&4Q^tpQU2mAu+_-7jr^=7rt8NAlbSSjJ$nN{}pX=BNV2ho8K$6u>!agF@esrjp3Ip^uA zwk#olf|ko2MkIH!M_y5r(-_w|(~vq3<>}C}++?Pc_@LkH;NIj~!r{Kt>hJVT;WNc+ z5WgW+ru-S0^4PC|v#<+0aN^ANVi1Nhj4CR%UAqyrervla1n(7W@R!MB=6$J*I=C&< zNTmtN@AV)R^h@B?vy(R0p8}}AGkTJMBAB@dkW6l&fg>bTowFG1QnRk{wx+o?69LXf zjH#m``BRKAl*eh}=F9>)J@XyIUl#5>QfUw~YF7i!OsN@F-lWiun0j7J0q&*TncU50 zuR8z3D`Aj8z@C!KNfAePC(G7MneL9^FnJ6WFSbeV4gt9e<}%xyz?R(1hg9>Jrss&G zVWF7OID(@41==Tvl>ABA}w>FCuWn(a;u3Jn_-#88SBP92mkHT#xabRHd!#<$(OhjwF z@%v_e>70(R{@d@drO(+X3sOgUv79&04EdW~G=&fsdMGDWDYT)oW-S(_#J^%m5`urDp^Bz9Mq@CXgsEG;k-~A6q^zg6iL0dVhu-(sr?&to~_cP&VSHk12 z{RhFz9sPnG`wz$Tx5J95@5;he-~O3{#UTJcN*OuY zkfAyOwQ+>N<9@;wfg6LyRTT$3*kpB4oOd_@75<7nbqY#|CF-HN>O*nIp&MT`Hy@Zs6xtu+ zwRi}b8~*KZ=iN8zR~^l*Gtl4PI~uB+eXZ}r+V8hea}TrxQkn`g*S2|o(3eYh^U-V`3mf9m4Zc2G@0h}V^j zvAvw&xhj-7KNnZ62VpQcWFts!JRF4$!E zX8dhYg&;EvNEW9Z0M#$pksp^*Mw=R{U(cL1>RA=BeN#|+0%RHqx}IzNLACTvrg~|H zhQGeYjg1$S0$YTK7i8EQQB=baj<> zS3QRZ_gPgr;>?3X2G?VqCZ3DPSM=F2f}w$<)Zrf-nO2b_p$-CLe#K)xWS#k4jfV#* zOeRvn12Xqw=%Le??ZS;g6RlFrRwBw}LXjorvK)Fd)eAsv1*;Sl(|2@+6C^QWf&qnY z5Q6c~;Eh^UxDAi3#jMA09!>VuvLi(8OOn^22|q{mB{sZgBFdWdd(WSEakAT>J`aDB zi9|_iJ5dozFFVDJjmAG&!>6PO2N_1f!^{;)Xp0P;Yy9dgz7kH>HR_R4DGDL4 za|ntr5Lu_bI-e>M_gBv#pn`+Ll|MHFh+0F0-bM#6!c?=Q7AA-+R)y+Dt@$4VKN8X~ z(RjMJIOGv9Sz;_$Z>vGvm@cF?Z!h|!3O-#attq0eH<+Z2QVGqVNf@!mm6-&=C)s-5 z=U4HxFi4g;KX^#aRhJyGX3%Tt71V8el3VkgKwTUG56X4yRi00#H`r+vl!f?{`8#|q1KC))6-lCstT z6FbnV<-%D+^U5e!uKK+x?AEbxe$+`@%rQCceMeGJhWfN;Ms`S$9?n8QR~TThkLNIX zl{Y+pKDpE?j_@-xKiYh-ddV^~z#z;4Gu62rYOebO;_qo=v{GZO+t~9YL}5=wxiP@i z2?Ac1P&c-mZ~$ju*0pLuX%=JjCjM!r=l_|lEZUy9!>TY5LMB);h+~YR%rX6-9{w~z z)5xSoJpl&<#%A-&;*b}Ebh!%FR*G+>j}OI@c#1s?7`n!|Bi}#EI5a*;8 z>S=8EYf~8DjyHv(21&1$S$LHuLbP2U@rWg%O#?U{a{vq$b|=DJXX-W=YLfA}NZLNf zw1pS7wS)r7+^?s!gf<#AfhqnZ9X5g~fsUPYr3^$5C(G=ffsQ+e0tM76tl@LS=ChAo zR#Je$;VCN^HG(NlmR^w|3r`&n!FFXcm1;dsei!RL{Nh_IxVxsdqGd1PRr$8Y?>oM( z!Hs`qe#lkyR96XAiIt6cv8-gw<;fj}smNJGez@rDoeM*6VY!1;Ld<4|y01wVhb7rH zn`#WeB)jk$&W6p3Jk8awqN1YA9$fu!P65pyARav!)>qJ0>|X>jF+m9b~pWQ01eRR?aesqv^9EB6ZVbRn-# zXCoFVj4Xe`wDiI-s}aKD3HeQ_D-mokoZ%K~&p(=-CUakB2Vq`nACjhA(dX4@r$Ozf z4!>DJ*cwj1HwAWL9RenyV^|rRN?pRBWP7EYh-dw>*qLBjAhVvqDTAf*%*xcq%#yKt z%ESIuIc^aOQ`mRqnkrE)^h~et^}Ffx)Xh;LS}G-)U%A-VYwQ!9&|^~EoIY8W=Q*C0 z=Y=qZFNbLrI9%TijFJUyw77OpXgeY9lpx_xiM|w)V|H3ulg(r(*&e@Ri^-#}!$V7z z)%S8vO;Z9_A6FtKd`F1Ad|vUliefUvOiwZMxAY!X_REVav4 z{?s`>smrK?Ja{bdi7-!s$=UoymfzkkX^U$&thv41VX9HL8n!7QN3mH;%>ajE9v0W| z?wvi|z1A{rzdqcj4cP7TiG06QD|YK7;O%XQ|8O-NuVvrhpi%KS?tBM4*LyJI2!680 zW9T^U<){z-14tWF{cz^K$9M9$p9g2=F7y`j#>N29Lkmy%lMP)*$e0a>qC#ZiH*H-q zPZYV4Y$t2BpAs_HKFRNc=3~=nOWc}m#9N}aF1MjVgr+fj-c^rg8iuT8_spS#`BS;; zcH7DLx^y)DH<~Ob5`r0nwh)To{6gGBIo-#Kx|`aMbD^D+0n??-q&BB!0R#fs{?wZS z6zbEYN?}ac?wwqj+W3B(!1W+lHH}Xn7qbN?^CExELgy9TR4{}>SwKI9b?x1imj+#F zzgcnjCSUTm$h48>$5#(LtvzCn2EYd}W*kXLY(n?F(h(NvzCW_@9&E{lTD#jm(6#fS zxk<*ExmHvA#!#m|I6lgY9$r6>Mp@R#QjS13o-s5}yFpaeLxrQr|5qKu2W+%jwMHpo zGoRi>3Ad)%_T+JxEMGTSmB{F={)Qd7tAK{7aohHOQ%&Y7<7sc-fXs{ziiX@@Tjvom& zcoQDRyl5aCupjyUxmqlDn<4u`s9-zV-dftw-8l798C#6K{r-|NiQb@62x_3~(O!Q;C6T$CUdkf3;O!*su zgK{4H<3U~r9yvO6E-AAh_4=}R-}2H+>=!dTgfVE!a#68?ZSeZLAWmM2L+YqO{#1eB z&%N7kMnM6;UXBIt=1|%PI1Ahu({aTK<4FZ+^qFCNb@w!%Vg*w%HJ)zg1pmrtsM4cl z*U-yk0L!hLbE8&8-CEQOq|82873Dj%ulx4!u?A<#~JQiB(kO)yO5`10}(TkLrknL$hX&q5v6CS+D z)VQQww3oY??3>*|^=6+Q%A+$Yd%f;hrC)227TG~e4Krl;sy2zWK$0jO^tUf4fj@gn z>P&`2mF;U{R;QS;;6$(ZcIGenW+!(#dz?JF(#Wy0$(bPtot{X?uh)6uE7fI%(o?mQ zN!^~-D*JOn1-1}O3je5%(0&rLoGD~@KvFZ?GbO{KPjeTu9kqI#;r8_GnVKoB@ zll53jRVe;Foq=rqtELOBj5)X&>&_^reJiRp2t zakkv#MvQNGm$i<%z0C*=ZlfS-v~uQ}+tP>d{R6{BnQKis{(|YQOx7n*%7hyq1t;sR z(*tnaw60CgP{&wh6a(*DJdCt1H77Jev+IN4dsMIL0p*hpM{28Wg~_UNem+K&9;vG4 z{<5J^Y|a90jg63{Dvv%&dne!Q<(I%KVhi=k5&yicF*=?*(1K@(g8js+YvBsyKUDGgYq`A1y?IlYW1i zF7tTAF?mfAZk>c-qcWM)s1ExjwGox6!0bTwXUa~K{4hv=u)-JzAT6csidLq#vbPkm zuyczzZ?B@BpxmWLZ@g~yK(=RfJ8~+=YE3O;Ze5FnWv!z23s=!+WD6d1BJO&O+i2vs zrH;0An1LyD@_r$GdZ}{JYr7=yy=HBrw%4G!eS(Q3)?J(#z;7JW{YwyMRsY$zE74r4 zw&#Vkz52@BT!QO>)Nhygq^rt>3S&b1`PgmvQ=-}sJ06|Q>sjVKrgs`twY!Yi?s)3@%Q`>qIBO!D(Ih<^2#C@8Xda$wv!J*5T zuTdN;?^8lVXsPl|_Y9j&4aE}a+o!_$2ldC#-5cK)xqmKTa%Y`kdfyO8F0H%e(Y#E; z4`sy_wlttDYG9k{&_U6~;_+sGCSp*5J-G+pe4%CGMxeY)!PvHz-T40f-7WZ5r#NY) zy|oJGAbA_bi?XXT!=8AiJUhj|Ez4s&ZDItPl#X$(&m;^C+I3>@HE!9$IB{p@m-pVd z2RneKvV4Pk6FcE&4SiZ}lP^z4x?NE*oSj>2)+^O|dO31Oi&P!}l-j4Vl1fo9Q~RAL zEmM<{QhJeI334)xrk5cL+BrsR{*js}IRph^6vhppSM?ZCoS=_%LI}f-K)CMLXwQtS zpgmG?9dox$#eGo4^#@M9JLAZnTkApUgAUNx);3w3sslD7_7`$}@j95#s&QP3=ziVr z2PnFnN&l3H5o_<#a}wc!j(hf?z__7S?Acv^NXlR1p^i-D_=m3540j$e$8QsHl29MxbWP~($^AOwFe#II!s8LDaYJa>c{-qa ztQ?x1(gFMI*mB&%qO*T}x*25e+oR;9&T@}qdFo+?J~wYzsviH()t}$Ov0r=u1{Il~ z^X^r;2ini!0d@6u;nCb%VnN?XgQJ)27F$*vGbZ#Mk^fy$LN8T1nv>C!He6JyCN<(^ zmu=-ST1MYw(Bj7`_Vnb1j7yOAHFK|Q=%qSmThw#A-Tptztcn9PIROTpNH`oTbxUh|TO%rdK4N%Yl&92`P!G+f12n}16jK#w)91`ekOIe* z3D<92+;SqIxSO#PS}1Gpci-vJbWw&K=~W6s6PcDa$R?psxjp3Y`pB1pGf=PZ zu^+*%ItNx*Xt#LU%tS~NIk)b0Z8l`Q-aJ9aYcO$A{9OLfq}~`a;o=E%9*JzYG($Z-6gA;iDwA`K$yRDU zHJzmV_9QKPY1k`NsFaN7G88%EOm1dNkW~xVo(%O!HS6$B%)v6bR7YMiG2p@^f7PvA zts1a{Qc}t|=@DJUd703!9|XN^2{aL)-0A#&Ha||QOh%L3gC7jt(;80oniR=fQ-(|&jyJh3&hYsC zWKhQgilx(3UhO?)GGpG|VR0)JKL1Na*)@vl+UqrcO*!=Wjbl-4N1>OOgNu(yv}2VI z@fqpiZVTvUdqRpoYK`9}b5giu)IWSv_L_1Jhw5XSe(+kLO-vp$ajddR`8e6r+GI`2 zK{8v8LIR_wrHi_*@s09~6ggYds_J*ZUY2o_L?wB9S!QnO5Q{RCRyc?PyS3D8*R+Cf z3#j|s5Tlym4P%Ve`&C{wHD%K%qF-6#zwh4CrC*}|p{W>2qYPHPde}>ko6M7?^-!&JPUnRN|CxHJuT*$RMc4 ze7p`~P|nJ8Cf7rf9@Nbh{9B9$C$3C?EW?n_$a^rIhCTL$(2EFrO=dQqDEfz2wETi? zm-Lg}YLd9^(*jecZF5&W8H~7OTWz5guXI4wPqNl%RNtcYoY4F>K$di9d z(jKNPZ{uR9tjJym;qq&}4e%a*2=VwYW%=cX-_l!E zIHfYs-QvHL<)MYvD6X;Zf|{@g>Y7FDrAYe*|I+%|*OE7n^!tCD?pG3z1I1YrdN{16a)ccc8$qS@_u={ZQkVOA82W-c|1Z{aw0*bwUr;+C;sI7uO zSoAjhn3&D(rXHN@b{9A$tNQ)7hPH-j+WOU;O3Odx4%$Oc5&|ac?>D&whXqr_a6QzgCJ{39i$^CNEHi0y^M14LO!jSoxsjvz?rZliN_d=a>QbvV zTJT?&jorMK1G~xCM2^Wkm}!+8@jPDP`Y>U_{Xn)jRO61I{LPEWr<=D?O2(# zagWVV&mes``P;T$4})BI9xt>$`Kc_}FHQ5F~w!k%T*}RAuG0skN7-<)LV`#2n|#pMeGX=xnY$#c?L8=l5lX zERuQk&HL(;`aEIpl_Rp`(bK_B$;Xhm$Pt1?P1#fy65|ye?A_+}${F^=gE^Nrd{5LU zjNk#&lGpPBVrO-0mtn2#D}sLapCZ}9qjz#E&!)?KETHZAONEeyD!25?d6v5KxC_dU znY#id9Apcf&pO50nj12#gmX(@3|JQGO+m+gTYClGZq&4M@E&y~?Y^No2|A{xV)m4x zhuI^L8i@VcW29jk7XD;b2?A4@WPYK zyZgRO-{{Z%(ya81JSOdqz;9K#ZWWspp-?~bru^U&PGn5gxIvIc<$5>XC~9;+8ioFh zVw6&r%YBE!t<-sh({AcN3pFq%%Wg}U6pd)u z%VEbLMT-`|5=O6#|ET{*(6+0<6J9<*$aLx&cXPUwE9jj@6b;*yHk?lwFTv10Z6u6> z8eQ#~x8g2drK8s(RJprl-8ys!4aTd;>XEhf)K;rTrh)Hsy<2hur5FY>rdo*t)I#EV z-n;r$Fs%QE6er@!htud!Na*U#sJi~NRVp%_xfmI%v z6cb#Tb)5SsI9y2Gg&$`Wa;hL;y81``$emV1H7Xn#^+|S+p1YB<@}(}Xb>*9HX<(UF zoz!wr$%-9TMY7K@p8TTlF;K9nBW0AwA@J)9%6FyW0-Wd7liygSHe=KoUp6q;0`H6t>LyF@yCV1Cy*mm1O8^@R`;^pK)24J zv5sEK?=dM(;~R16Qv(47UlA72#mIJi-Ia z@C)wh=I!~aVCsTS&F|>+KGD39*pR^*e+-+tb9r}mLjKrT<5+Y+@W5k@s!z2e=H=`P7jIL}Evvsn>i)Z9RXw(xr+-@P zr3cPhmjO}LtE^0I7SJ$5{^7=Scu+rGZO9Dm5#w{zl34y;EUEsFq3xFqu6m=$9*b;R zEAQcMh7FIw4pHL*)N?ic#_cc+dp7F=A=>`6CK^1sq>`?gn`Y8AkH>W&jGIDO*=)CN zk+{Ik`Shtt#HuH0-8!w-xc+A#CmCs|X>dDO=7BbV1IVYAIo7;Q% zbw(=E1An6Piw|BM$sx$ay@vp-00ia5gzsvJ8dD@g&wjYyQOm)%4i!0YF9(h#L5*D8 z!&F$Ensv*?$*Q}}KKm`82id5&P*UA69$P$87%)|Tf3gu#lQMNVPpkC$cI)7}b3@$Z z%B9)VuFqRh_4GjC2V(n>!306N9jnRMCvp!3c&0pm;}NoOLw`1 zq)Ev%JBLxW)A_e|ayxgN*T+6;BB*O*du&EE5TeY<4Js3DIUL23Q0QjC$y9tgRzjPa? z$8I)uy4<58n5szvCzo1ATdkW?IlH`HH7Kv%N2Te7{$;G1F}vbClDU0ls4=M+mF%O% z17CGG0hUpe$qHBWWv5OKK#xs^`)qnHP{($SK2$LxA(Yz6FpTnmBH&Esm>D1DZ+#sUit}t zu+5UFUug6#ef|Rjh|;Y5I-8qmQ$}Bet_2r}^6$%P;TZjH_$0K%B|gS5XZfR96{s_hJVSvKzswr#Ray$^3* zuDGe*#aJ9dHA%p`^!LZY&j*20QYNI|$(e3u+F^kY1_ZP~C@}!Tj zg-ANwrZ?StZT*rT{DC3I*@kvFCaonLt>l?~$34w`)b}Qct+iXmA(vs;Cp2AuD?MZp zLZfV^CHEEz?R~?mpAw!=5ZM7VbDMz+;-phdxfY%jHTC;?+MsV@<+sL;GH@ems8*KyigUz!}OERaasQH z-*k}m9u;bJ+@5fs8p?J}hK#-V-~2$_`%_pUre2dLeAgU@JBD5y6EZK(0Ux=u`bR_A z0ZfCBYFLS~vGc_R_do->I&KkocaK@nRh=0QH95-6t}j&TQJ*9u?Q=ErKzj}!HLLYi zHB;F4|6K3)g}IJx0qUnB{=x&KKAkL(&e^zs++zR{gVccS%BASY9n9E8k~s>`B<2*m zFysugKuw~k`J86gN24(&fqsxk;$^|w8GiZ6eAdb3$bmeyP!3-Idx64tp-F!a{~Re` z_iqzkD<-UW2<~?6zr1_;jK8K)h2>iMC2_&?P&Z{^)On7}xbiq~hk8U2gv zpC3Yi9z>N`wtvcBK5|9$Vz<5>VTs^O8KvJ|Q^)E;4^N)(U_Q$DkoTbF_Ic*<9}&v! z&;g+gX0JC2N^T^<89T~Ie;?mEmQp_|cNJoacv&J>o!2%d(XAae$x6@WN z5YpfN&rott{l>8aB(B@~T-pMFhTS~sr}%*y*8jETE_U@mx%}6)T*5nU;!MnQ_SvC5 zt6*wkeLz5p19G(K#HpmQO7kh&dVtPMUKnzX)58^HC)squg9*d`%=bt*^G6bvmRBqC z$*-Tl{VH_Xa!cJHoh69bS)@x2EY*ow<|g2E5^0%vMa1BLnfQz6&tpacr0kGsWv9)8 zh0>)4Za+y&9ZeChOiuw8HreI)8O5`g_I+nQKtRhfTC>fUX=)r9$HMGo`!vWORJ|Xi zt2QyB@!MaEhocq)ikvgyRxt-f{B&GLCfn9w|Jm zuW(K+<9^2*4V3ZPEsAt&FbjD~e+G(Ig#LfR{ml@2O} zUv;qWE6w$Uq*j5?KXNLLk@^vZr1hF4#s>YRkM^-8f=UD-6FceSRUr55)3O;3%-lPn zhh!G1iaVK@#(Y?}l39|Lmk?BOs>9mLy039w0lE>JX)Z3<+@@R+F)V8>{zg+G@5D!$ z18#ITtE?LsjVqK6Sq#OvX;1@-wb?+W8GGNg(AT$@Gw(g{>J!mdVkv z$hE}yQf+g9E$%yCPUF2pHE?UuJO|gzYt@96b@P3(p*pJqtQ?xO{3s(PJfm80Z|MJF z>^sAn+_tTCTM!XYQ9uz;K_XphqzfojdhZ|*dhcCDKx(8DLhn7&J1A8k^d?<8gc2YG zBoO#sbi2}@V$+ZhAEL5 z8&|yTf+cVOBf%q_U|+d7x%?|L-*T;-^Y!(5Is?wBN+V{rW_Z%wEbo>diG}S|E`WB# zK;S_YX}6$jWx^ctgAget`D<3P#h?wI=i|0_UBZh*yF-6;dQbUZN3u{p&7vY|q%mn< zF9mv$5BqiiVuZo|+#Gt@J`2|9g1J;1Ni6C>N`K>h3n8+r`a*CAw4N@$5<^3PurG_VFPHlr;ebax=T%KdeRw zBgTqmi%ATQKJz;cCZk5;<|x8h94tExG`!v7CUqtI&9r?Y8?(ieLt>YJ#&A;)?!?f3nE*2pG2onrvuqd&EaHWq_3g=pmad&bJ5I|$nSm6i1>1V_0Yx&XGkoP%#`lT+2J$8PAXYNvBLWj*I?a^#!m zBI}ZK66@7;_?WZBK{-MVb@AZkC~H0nPeu_SCW`bu@Z{JCufsgDsad-F6VcFSnVG}3 z>Ji)w!CwkybjQqzIqSGr)!pNTo_pT)9ua*Q+$dGb(_W(6h#KokPw!4&4(Kzvp6`WC zYvhfLsX*A6JLYgX3D$DiKOkMhFK*b59N(5~cJQ@?xY^wP*S3h7_Z?7gxF(8T1YGc) z{mZ*X;so=_H5*+-CJ` zz3h$nJ*K(OE10WeaHsmMDK9Q~TB@*&1XQKD$psM$`Psc3*x4cb(GvPKZQQ288DnH) z!Ux@&&XO0xYWI9g@!aFF+@*u&`yIV29D({Q@niQd&J9o0_rqcaf9Hnmo0!ME*m&(%2rrfn$eRZvwYLX>f04j1tM@b#c zXN+ra3pZZe##_#PU7~5Q%*A2bMw(9E9J4bFkjf7ILNnrOnvK~Hif3|aR$73R!SlP*t@ z3#K=Uovm-K$HK6{$%?h}^cI<^vfZC;*I3J0%PFT2wm5y2a^FI9cn5;2va{3YTCtJJ zTIO74WT$@`PPL+Af9IFic)%DDrb_!LDFqp#uv<9UD!g6HwSm5(qQWAVm|_rLO2ot8 z?0fuhDxN&crPElC+{PVf5QxO=|F=+k)>?f4UHdD#3--rF%fg}crxNE%=O~$1b(_7z-9~cNSHnBrB z4Iy7&IBrDOH--)}boAb!CuA(4Vo>Tt-mifPrBG@FQ&=WW7-f=iOo`U5v2Bj+F(4%5?`ych1+^KrlT6^=mmN4J;RcsG z>M|ya3X|8VXVtYmU>0AM$8mP*d#}#Vy2f|+O|@#+(WIpGR2W#k*fMT+X=PkBVt=8< z-)Nx!X$-jO6Q`RT*l4h=nuy~Jxyz9X!3ewXjvN+YXOM{LkVLDopDt`P#$c9QNIsZh z6}nom&t0a5dZAkzKU*H_cY}zS`(TD8_x>T-be8uSL_+P^aBp84_{{>N(qnXrW8F^b zSXrTzZr}D~-`<}h`q&yAW(u4iO5>reSLIz__jAw&g?F=4Zg#~-JNV)HEvIhGRLkF! z+)6Bzg+Nk|)(#8Jgirmu&gSB`{Z0sme1cizZMdjZ&OV&nU+VPUV#10BjqsFR{_RG8 za?H7oa*#oO7}-9UD`hy(K(eMFRV1aC3iDy%uVm(~0uEF8`Mki2f;gDPH+li=xGDjf zUTddH^+MKvu^+yCAW#FV+tR_B9OMhR;w-ye6nd$MLUpCS?Ov~QD94Bx@k(JGS^}&@ z!{UQ>59}k3$s3M>m~8JqI?FpKo>W+upbAPuHQhyU5zIaxeD*O;A=kQsyNsSq7Sb4z7<+UGoSYS+QP!!?448@+760n{7)HwMu zaCs?qyz_%_{xCtyZrun+*~@dU8Y;IqW$OvBUF879g0x4#u-sZQZ02))!?oR9!kHLx zaNC_`m?^lugXnVA+}GX_o4HRgA&)&1n*NH3?ah8Zp{C*L8mFG*FFjVVLavY67w<{| znN4p%5WIb(s;zD43{m49j+^Z->4VlE1!HfuMqfp7vlx#ah6XI%jtXbf$IlKA20%RXm;pdbHHX9 z7(=pY-}1X|CPauIH}oQf@gGA7nLl;+PBOYax?B1)y5z2k>uvUGLcjHv5#*5DvbTfH zH1221M%yAS_vgjCp)ApXxUu6l9?x{`%G9CaSFf&l@*j5*Z@i+D{bYg1KcVe=;NoT(BU6D_#cA6K%Mn8?iE#Awr3g~V#?f%wkF~w6(Pd46B3^j>RuIS>H7=q6h z6ZC@=&Bm&SNMGFb=wMSVJW#2t%*%VTJz+N5e6-T5&n24yDjR1z3!G(8VFvU&3MR@{ z-_oG}zles|ZbzUX_-#p~c*_vyB>`_t_oU-hZS_R+_h{`QTZEU=G$O=cQG9$iDh zC(R%P@l|lG0lj}p=xgtB3_!gM%%G#*{wdJKZjOMzA`eLaRi0a-;>I;I}bMG`&pT5a8`z*s`uvN@Gn=11d{k<~{GyB+zyQ z#>vV+<*g>1iE*(*jjvd}>8HZg5tERCsRoKIB<8_Bjx_E+fB!Eo0XtNi36@JQ_G)9V z*P#B%_q1`p&zHHTL<}LK)W25{Ne-Xbvh1qui{|mPQEg6yw|jlir@pH%J%)%$9FL`7 zDAfxX)+yJIIFn_T$&Kc*{Lg@6j56lG%;AY3y8CD~-GZaub$# zx4%vzYqub6T8{aa-D3zBp8DtpN_IzT_&q|azT1n5;5!bV4Bi5-7aCGI1a!|&$uF@+ z!{^MeEBq~~R&8Djdt&EM;(H%*ssLz>An3j1s5GoSuY-4|xi31AaqG^=bao@5M<}#S z{0k46PVd$HVfiLOZ^t2msgKO6By+AA5?kE-@Bbd-n#sUI)Zw8{6a@jbT91_k54v+r zUnCi4{I$MrVT|19$@VGXKASd-?H=dW%j;`MQpT%bbVv|P(A>W8sbF9@__aapx{pnp znv1>#JZ8lJkt`wk2e;Cxm_ssTfb0%7axDZ;jbD!$+1O8yzpNEoR~gQ2WhK*NgR{ja z_&qMXQKEG{AtS`u5ta-nR^+}SFu~*#w2Dt$zLVc`GEe(0 zKhc|O2nk9h0Hal?T+tNJTTb-{g+aXNThMn0gQp-s*oZg`n9F);b4e5+LY?lhpOt+m zq?3$z+0KPJnC7N+(hGx8*V}urrhH!qtFx)D@^1p_14v0RCY@`HP=F^OCgnjn#_IQC zrolu)aAh%ZRAA)1-;XQKc3N;)AO;KPpRSv!7i^4g-+{WxL_7M=0=`02aqcn~708kb zSb+Re;xN$E6g@*nQyC~%ujbe;9a8x1Qk<(XPUu&n5++2v%?)mn*&UwxzY)Hzx|(pD z8eHw?*Pv9QV+;HXPiI1NG(H|VzUg!LN5&$dEeJiAp`Vm=jATcsY`AV}0KJB9{l+zx zqw&FD5OyQHlO23t$*BV{TpR8xhmdS+h1b(SYG zwq0>X&qh7?u-MVXi$(^m8hP1+$@&Q2bcZ{1w({avu}fXigAVyDGpg3Ux$?=%zd$t{z#3foQyIg^r;#1{%T+%^Q~8l2^4PDg04AwB9Fct5WrZEPqQY2nK6n3N7-43HFR~)M9~E z6hQutAQt)TfdC4aAU3^BUv$$}Hl`Unn~XB>^R2|00bNy^*#^npqV?Xw4!KC2&!_Y< zeUIhXg;D^%;1$b-3Z=`V&ocOZb}X~yk&D+U-d}y;{g;Ip7+F;NXuLhHzk;L&mYr+o zk}5c=`O=NaGm-vozg`lR*3~TOnaMl`{9T#WsY>X$n2n37ye*SgW_NCrnrVrH&vAzm zbtGf9Mkt`?xx!P?(Y9v~Ceg6BwM{%hS-VX)ZXDM-hM2II!vV#)MeCmJHc+rqPFJ`W}vcr=iCM09y7!ZZPky zdQ2?eKK@u(*8IV>VkisW(jz`)akt)y)p^L(V$vDjb|}6av$xPa3@PAO;A5{P30SKc zIFJy7&A2-DZFq^6Ey?&fspE5S@Ca8ul z?D!kbfqR>|?k=>~`E5L0ZuRK*>c>8AcZ=-1_jqhTW|>23SKta{Y%rKdV1&!A_BFNl z;_66nr(J1ykrB3Mbe)3Ngz=$yMH}c7!+tBW2|KIzriRs4J7CLYAGMosUzW1jEP`aI zlfjNsp#>D(NWT=w9kFut`lDUY4xU;zftT9g{0=slI)jGB2(R$qo3UxlYvm+4 zJK3_mZzlBJr$Wv7JHno_wqQleZ^pg0^h9!6McwL&iHlq&I~sYWm$xAY^)^-5$m=m1 z1rh2bw&!v#SDRGh{vS*y+b*yRH(*~P!^Th=3x%9{8b4Qd3ij7y;*w(Yp zP2b_H5T=veTb7)IKJoYLdJ=E)$QK4GBOs+wr9IsV8jWv0F>OSXA*~P7MBWW=IN3iv zi`Q$A!4I|`W1JE(DK4GE>fe{>j`ugdm70d|l*yUDF(G7RkC(rZ>DZMwimcI z*1qL#4@n&SfD0q!?apefA!PFCQ}ejTStgXv=qmJrrc z0VrgZ-nzTqUM>*_q31p^2!Pq9WJ)fMe_5I8a_51R@B3)m+SYb^EbV@4SdjqpB~lvp zO!H?{MJ{XIG@Ey`;~+R;5M>`Jhet6ONU7XsM8Di#(AVCqA6hjEwT0dqxT=skq5)>G zn~+vLrDqX=>d?+I^pzUIYp#B!h+EdPq;dkNi@IWd!ABF5j&6A+kb0u=_g+P|Q@a~q z>e&8TzJqzgq-UVE>ae#r_Kwk>I5(<$bDIJ$(J{`Sn=_5Dm~c1n++)!LHQtIH26YVt zhOuUL!|jLRVF6Gp3dqR$7s#OBpUl zM?XDkVLo$vcvShuVWCK)u}U!~w&mOQM7iXl6D zFq`+qS#u4LG3#6=q}fpdZ^24e5twvRA$1A0^+iSNc4at=muOOtp1GHtSlvf*IZ?yX z?HtA%=2G4cv`?!m6)6^;dGyE+d6YU13L5~B1fL7>3S3I$kyF_FyB&MX%cc>0tIj=& zc^S=Z&hk^c&nuGS8!=X2h13hT9<;u)DCXS<6#Y;HtjsnU10I}!d;;oINOxO!R^bcU zPt%Ae~#$rAiin;30~x$)}ba#&UDAl^87enj4*{QPQKTA2A!gOim2P$ zJ@Gzi2o$JtJV;(VI;+3B@NB@aXF!qJ{o z#4J}y>PX_5ct+K&WiQ-9An5q0WlT$TBcRbl)!PG9hP=>@VL??-Z?UXQEHqiaG~Y!x z2ygH;M^+GANrFX@R@LDyl+yF2Nz@iDaV(H-4zJGXgl zd+4ZzKp|TUbQsj+NFIfIbgSL1XGuVTEm!v|e-=}x?B26`L_UUb9ob)(#D-#jLxGB%NJIzhUbtoz|G0%{1V)aMRjJ}8ycA;`h37rIx> zl!vGMWvme$omc-bG`Q1Sk?JM{PRoUGuA47Sa8xj{11--Z+oBQ^C(hYGcJ+Bb;pOR7 zk$J~6D&1#OB9&r|i}Q^~e8kQ6oxCpC$%S>7VgB}kC4usi^w@L2!mOLk0k+f0 zUeBn z0EugSY<*9ihW*yrw$*6rVfteH>w>Q79o#Y;vHzZjPhF9v<46n^X51t^7CQgf_I4Vv zQi>$`pp1%goxS`b!bCyWW@4GSb1-nssX-MysC&(H9FnbhGR&99y6H6G!aS~KNhZJ& z&K~e&$n=>)z}E8|gZDmdYJ&!XP~8%xh2{Avy&CewYU*GENu8*M?>s+PbJ^J$LhPt_ zk~`z$r%9rwH@Kmw3Y+}oA}e~9x#t;zq4w3Rn;bHn3~GRZB`W&;mJ?Dw);ZKXJ+ozE zXeRaD@t@2JeAyYS;JX)%vl|KorhrN&9A3L`el0x-kFZCuO8znYdggsV}zf) zb_Xe#5L3D4D#&}(5*oh==jZTcKr;a2NIj{uh5Fo$V)#QHa6EEzu1IRCXJ_xpS#fS) z72&VW)-9%uhg4dCDnuNEC1(-BXUmEem?kYE1gGvxYgf!qOt+mXw&adcD+DD;HQ)6<&LdZq#hX`)K0YiG zpe^Py6q?W&?@2Gj$bq2ZQ-XH=wPo(cixUeeeZ66h4c)7Kg@!%~-!(|s!0ZABwMFmF zQN;Ro(9KKWGPS@eDnNieB3Z$iD^`_60oQ8<>4k!yj7y2_;?-%OF}nWoSq>d6nWzrg zY(JI|AGa}(C7f55ee6F<{BYeGz$wXLEd}OYj;Qp2>-fHq>M$P!mlW5%jutNN) zb$?pIL@yk?QAU}m-M%V($Y;}0cs_YGJ&^beBeUCMi$i#5lGUUy&qL;j5tqOf3`KB2 z;pqtDv>!6pc*Kb3{s~4``8EDrz#eS0tAbh=>%rQcD`dvz*~b3gp2bx1C_tdJBDVK5 zaJE2?RmLd z+oP}G*Ub^DxjHuEwjKz3vI{r6;h$DO>&ux@Wl>!QG7@q!+>a^PTIn#a2&-4vzbIW# z>;vo&rKZ2a3VNWUZR=oKHI^2pilZtoad3I0LQ|LntUVCW+QLT^ks6!#HhDpv4>t^N z6i=*5^nY0df1S3B-4u0ogmi_e;p<<&Jz5y za&Zto(cx!MYFM648i}gs)}8l(y2K^xluk073S7nuZeF$_UHz0}w)tehpw(=DSfrH< zKR2U^xf^=6xO!cIc-MH_SMS?^%=}DN3Cw11M&*s!5P_fbh6MgYT;EZ0s&=~X8pdjb zZ47K;GLByFC7Z}uzS@`$XaFuXX+N+s37>8DjWstR97oRX{k+64CkYJxJxtWls|;r9 z1TDr06FOrSk4F*HlYjFFHv}~Dyj|LcvrT)$O}g@UizV}bHIeWAUQ>%1potS6wV~P^ zlB7%5XVocuuJk;TzgC|UNb0jDo*^ydxb)HV4Y6=<5bOzY6zqzBYYDzT@9xw={xNt< zjnsNJw`!}EtY0b11rqo2YMk%I2z{ZFLIqn*_WN4zUvHqYN4kBEBLTun19J6M`5MzkRFjxg&1|_1g67j&mbiaMx$iUX z5@rESgLAoOZ$SI>t?Naujlo%p6piuC?*%39p3&Ney12z_CfZ#MQNd3Jd66vKmjC^m zssg`N;NGSw-Aa9tIMDV>m~=+0ZX95Nk)1 z9WT}a^6nfFwkx8okGrxu#1=gid#yRqd9_N?SGe*@)I&%peQliE#Br>Dmqkv+@h(P4uu4 zO>@Ck{_#!uQBLg1IUrs%fZw`tC=dC%5-;^;E# zv6@c?k~evjZ=TwcFQ_(octYs%c(+kDg+FWdXKXks)u61K^YdJ-efDX|<}2odJUWoP zX+D$5jbtNlZHwdX-XivhM73x^L5_fqZ+oAkI9&H+TYEgD9WYLvflxUHzWCN+&u}P3 z=U$yCKE_1ZIDPj97g6HdfCm>G{x-uqm<}X<+J{gK<%qNA zojJCupms0yn?1^(2Gt)!lU32RUiG{+My6MXcp{=M^1PYPcc9i-ewxq0J)!Nfe07L7 z+)@ACoFbRvUK6&CFAU5?sbjRo9z$VFJVZL z*4=GwA1GQIgX_xIrfss`Ww2Fp^gQ8xO zYcsAs$gKd(GLe@$kVwdohrAs332AM6Tjl9=f(WyI2B4#7;qD!k0C*+(^w%DgTqR9T zD-LqLw#n&Df6hELb>B&+r2kHhRkbi(Al2s=1U7+CB0?GpV^jAZK2N_c9%LHydPaYW z@b}zliuvJSjMLDDR}e?tk^dCQBh~5i1?kzA+SW8rY?o(h5`KzHp+>de@=IKG85hYW zp&?q`;PIe`Y=-fT?&`L75}QFD8Y8WJ!+N@`col{1V8Fu^&hiNp?;`&;jR612NdPn- zDqJJk)|d{5H=rTVEBxkMz>&?G!&{EUrazlJP3g}fRdyFtOtn2kRPb6*(K9AdP1FUF zLwFw^jxO*^xQ<7Xp}v$xqk{cAM+j)Avkmx8BYVcyyA}zWKC83VtPowwgZInC#+RmX7iMG*Yg~M_x6>)7^wDvjMT-$+ zRqyYEZ4Iu`PZp|rk?hy(Y;FGxyag4msCJHC1v{0aTQDtuQCHk^>OC|r5{%sWmIEM3 zcXd)21B!ic_bU0n5)(KioUThY3peb(u+=qi-7OHD{;*p!ca$H?A3>!L6|E!fRnU4Z z;!^0BT`4{Fu-2&Loy%e2TUhbUd*9FHl$t$tCE=+)qWJnAS`9E#C{zBb?}ekm8Oc$^ zY|JUjA=|Ol{9BV%ceq|v@nI*?qIpV0&JMc1jMnLK=33&*)ugWtOx=<@oND(qq60ev zV99RM3_Rrb`&%s`>nms-`hjsm_i6Oe>pH14Zovk=?QqZM$W5V*!~AkK?Hs$wZ8_hg z9Dk37(XPjy*cJby(%3HE{-8YVZ@qSB7Y6R(A2367i;NgXKipE*ecwsTn%G~B8AMdz zsbN|!i}vm(kQ;E=maj+sO!LrWmd&T>XI(WdNRqcCGy*V%JM7ISmB19fpYJ#?_8oyM zY8*PpS+9j(Gqu7zzkHe4|JP+&HR3MscY%XI z#Jms}CcI`fi|}`*4PlhE>z?ZU?WUIkk6tq)H!pnjU0XK|j?4~j>6B&DPCwo6v%&TM zd@=A$>Q@kIlGD6HsRtm*X8^_1UT`TCu2+-!b3d~9?0%=TpSdj_M61i5d=mCd9> ziutyQ&`rU9VKdR&!s%>0yI$W4de@=)19G#|YuWA_QSDBn;riD>?(q64!3Zy12CFF| zhP{JD8>&r#G1`%pYJ(SBx_zzNsi*swD7|qU>eslgPJ@a!qF>e8d4HRq_mHAtCfvk{ zEWXd@Cgnnp-lysrJu*wIu}&#WIO}#|nPLm~y3W2=hPox)y!wLiWX0uJrt_zRfS?W0 zhVY|<(BYJnrf7tJLQ~LO(!4d#2tB_%M9ses&ZNEmO>>{nLB6G~lZGvn@BKZ*Z3Svw+9#2#&o z-z0rjvu|8<6LQ0zOLi_E8%}&ZGuKykye{xt?RIf(N4OdEE|&<<3fM6&HKyYcUtV-Q zT|R=wBXrq3rt8;Hg9yHX{p2YqWX}5#M!Pz7#X{$f&BllbnxYz?^?afUPFiVaYaOsy zLA&V}vQZx~?ViFp#-EyMd+@i*Y50}o6g>GI<~4hZLJ3zL+^07*(1X*TJ?&^fLAFa3 z_Csyc=FGM&-UjTm)|gkR%DG@4upWASRV>1|#p40Z_?*aH8@+mTlFx?p3keG)0*S~Y z&hnnB2gugxnyW`OUdfnR2GRS>&Uh-!#)^`e^)*Zq`?Sc}E}p*=#W4pN%jPdpF|cd? z?4I6)(Ie<;GNlpss8Q6Nb9s5pfpJd8Tmk$Af%J9s<$ud_Xdql0Qw(d&muwThE@cja z70bV8 z`);Opuj&hj%#zG6f_kgo7waZ|-~Fr|`M0ouhk0>-Uu&Ln$Z`4HBn%A?xWEDT0R{V^ z!!&E#PxTW|+cdpdp{$qX8cq`G6RvL!^Y(Ps8P%_Q_J?dzJYcrlzcmMnY9X{VE1z*V z`!G2+<@Sy{+h=kcT{vs_e63US5wa4Us1-pK?_RV|RMc0ATuuC;$+(icXVn3ml4SNC zhpm&XBgFcH4Vps9=9L#1e+4L!i13ma<{|#fhC*88!jsAXM6m}Ip+m#C=taFt7W{J_ z^g}fZE~-{35kmjp<3ZVL?Ium3>fBVy1sa(+iMDL4;Y0{L>HHoZpTd?k-?yR1uV7{{ z_oe)2y1UOpB=Io@-gqdH%JQYac3;{3kg&2Cfo8(B&Q#+->0b+N(vojXYNnHV(Eu+Q zihJtiirk6O@2OP^OImFaxe$Bv0hROV;}RP~x#zS>L6Sf{?AF@wVVxTD+yO^BQ-< zG&$j$=I6U@&r>$u9$xa}*Umj_9FIJkWX2j7Z}ciu|BCS8wncP$=Xrd=6AQ=S^Zawu zeD8~{XL-Es62o*r=XxsailOhll+wouDRt&tgGYratuGMAqX{aQ*btWqGHqM!UKO2? z)>hF~=geOrY^{TT=nBO3iwmv|gW56kRTsiU`av9iE^v*kw+fVN-W(B;j+XtEb4$ic zg0tpW9b0>?w4309Kzmmrc%G6O5QxTSAscQpiLDwc^l`UZ`FhPDI?@8eEJt_0zNx`A z5C8JjnN%B66bTa-$MeM%;=_vG-PkIvKhs}oTa+z-(4Cm<3<`|dU9V*cEY#xR<1JZg zO*dAgdLqa6s|297iL()W7)aV@)p6m*Reuq6K+x;pdLD#3rQ6XH__93T@!Vnbz}`+g z+v`^M=WA@q`@bB*jHYDW7bHoIJnbZ)XF)ka6jZM39m10VJm2u>@BL%h+5HiH+;0CD zP~Q9%7VA3H>&uff`YMD7C$9F(p$>+-WDy~)X`ONV>()JS>`6Td6RT)^0{>}S5x?g# z!T#y43mD}SoH_Sgga?4h&1!!F>J_8$+_i0Di}xVp+!u+QFD!h~v;eJ*yI$g=3W@D=^_}NyWZ!K`gmpm1Jg;~F zLZdmmklays*CO{P6J5q7AGvsWVGl;%S$a-?2+tSPF@O4Jx30*25hHo~{F(YBA+B-; zkg#~POvp7Qo-F90Of@#%bWj2MXHa*xODlEw zr0jV(lI33iCLt=JKMNpS)Z~kpb{6H9-Mhm`ba?jV4)^OQhWP)n7+U8Q6NmHYUmVKbbKY>JA4tE(4AHXJnSBoZ)iTkF# za!{+*w?P%HtbeTiU#c};5=MoD3E?iE%I6*5qHwrG4xw0VlMFJYNINI?oQ3!@kX*q$ zNM~s2Hc|L0B^Mld5p#7$5ukNkxPS|;ps}R-eZ&rwObihXdop;GLlN#cS?fSt?p$cf zgHwPHCam=&B)3qWW`en$A;q`%yQj;<*%JAElHb?L7`f^FVmx=>f|2q@9yH7T!dv1F z83EDeDk{{RQe=g2Siof0ultD_7@kZJK8B^L_504pkE92P#BTeY@^~7wv?=w|A|2nQ zZA&NWzWH5Bz3`zAxOBn#?<1gi77>z#HDVc&XM0xeU!u}E8VNsD6mo0#L$nVfPHD5{ zQ@c6;uYZ5@@@UjQXZsya>iWOe&eV6MH(E9Gl6C*hM`3mnCpJbWjEP2$4rM!W$P7Zg z*%(5X{5znG{!P4*-cmbxUv7k~#f!Z$s@S`gG z+td-r%^Nd!7)HXJi^QWmBpbfox$%*4ATX5mX%rsg1BQfnt$g4=M&4f@I}!jN5AfkG zV9z%w7E`P zT=lQzSfy-p$jB@o*K&kaz@GS%9c7JBCR?@|OHr`SDip8YLeVh_W~cfrrW6LP=DecV zutIf~vrDx&`pe?gT(&RP+KSFygWesmViS_QZ`bwwM&A^-{1bn*0j=h;+LohFMc#fp zvDtNDrwxUu{yY}idDwBrkSA(hmMYo|WWkl^C;4g!_l(>U1hfMIJm>c@oJJz=L#_gk zoQ@}lz}3jjlyH~BBO*Dns1ky$X1M{9yK`4DVqdz!YrE2rRQ8aBX&6Vw1bAd)kNY#qN zZ9$t$fzTbzi+KNV4xx_FU5UlI1ZKJE&q|wF21iE)#Bm6gQPUgun+@?qZ*HWq`#fc( z&h*NOrJ(L#Q8Sn{Ois+#MFi@)Oy|dl2PDZgIQ_TfVh()3gAbAo3`-wBs78__3K72iDP43EB#Enk zou9aNo7J#MQ~SO33Mkd;*Jq)$Ro6IunAu_kSNQ>DYjs_NDU*4naau~L7UL`X(bLYQ zyM+#i*yqGl?GSvT@5lB|r^>H;vF(J_E1$Th;nj#!m%V}GWt@G%MqB_PrkLI4bxLv; z&wn^Kx*R~s=4S4%6e4G}OfR$6f+r=&YvF}B-LDAX#EdLzx(J8!`=PoPYAoJ+#H7O? z#!7P*JfKE6J7r-KuKd;M*L3G_J=tGz63EsvY z$M#>ku{yo%@m=HZBPY_TBdp-_Jk~{)aa$+sTUn@wYZR|P&P|oRnAAAk)e)H4RsIF= z6@a?(I&S7tW?ttK*!&$bNsD3G@Foz<0$KRVvcS&JW6Q2YMHFvm?s7QHG7c8ezH zfyc9h!kO zq>8ys5aFGTEdrS^471yf?)86oiRb# zU9H0nHp9vYZn)yHC z3<+#4m<5m=52XNeR;WP^iVwGMw=TAYGueRr-5k}gDO8!Qk1jgfL%GZZ1Ym&{&f3(8 zIj|HYR4w;-V7ti%Snv6TFINHdGRmx6(x&YY+gbvc(H|aP zni?5UhiMtOCxuxgs|+t17wp8Az~o-9{;U|PA3LN9uEuF`laupT(`|c;h zCT1g%0t>`f_b=dLragul1LA(G9bpvyQNAGqZ;{G##yl$8XjCJVx_ z!4X?Nr!T8Co?1gpGi4NXfR~hPAGn6Wl{{ORntYMN(chcE#3Rgq3LjCj|0OfKHahYx zN?)+?@f1_xM6}j;UgFk%u$3iuj{^g!v(n>XNKlQ|{p+-;s?i(@kj8&@;Q=6sRyk;w zb^f*oFxcL={<{wQHnNVm7yT`1?Be^H-FMTeHDpmpsAS2M!;E};I|I3vOl4)N{B>l6vbcPJ!_Yd>5 z0VCW2%7y#cK(+Q?(;)yM&kN3jKD<^y4kV`mzBbho`F`1dGRl84LXYRYEcmqqys^^3 zobvYjqUQJ{d<)n&U4Q}`X|N4#yLZ%5&$3U({o*?GZk|CRoQC~$@~`0D-IG&MKfLI5 zx6v8{tVeI^GvpGZ?3TTBN==)6c zf5CF*|fX?e9q=PEoy4IIyzMhc?!xv4B zC0EKRAVE|G71q-?SB_ISJr`v&yzUiJLIngSOxRXzqDC@N!uOv0J*pRSJR0g)nvkA+ zoyl$c-_2AT39CGxmA&y6HVU42BdXkhYfJqOSJTIbJE#P7(;iK#$G`MhqOb$8k=Y)`lCyH|x8zN$pH`KM7%2cxECukb z+Kr^#9HBm6h11|q1&}U;gj$8c_a=%+VYI=o(e~g~ikSY&)e_8`(7%|` zIG`hCNk^{aJISWeWR@Yk$IP57rB0FC&>np&w^#-Xo~jUlRT?GcXlCuWua{145sK<5 zyBED^yvy|=4knki`rcVTDyOeN!yJCd+$&(6Qo547-xQsy!DRfjDf2m|W3}gV5%1vg z(;?}tyZj>5!-tpkY4~SHKDkDz4?GRmvGAcJs*EN=Zhp)!iR5|lDe_lgRjUGkr^(4@ zXk^s)jDVNmeCu0^)AlpYx4^sB9FenV_mhH#`yHdJ<;1Zz`(o=Yp$OX$+&A{>BniQ! zDmkI(E$>JzdH8rL>!#!WMKJM{-6UX)C+EhH)mjNy`Rnx2J+7+uq!-tzUS_V~WiVkv zASJ%u#*o=(gR3MlgH$4^RKq96y;RFvJ#?`H47U1Wvr4Puhp4@Vjo%r6xP-;D%%3_h zNLB8vugFBz`0c~Lb1Ww=Sp>yl$G6+RPq`ugzTETrqdglRGF}HbjH2#h4p@+;nF$f- zP+XQ$K7O0{61kkDgAHQ2{;Z?_eGU@i+U21<=z~64S7in$R1nQAK_q=SDMFyHD(C6 zy39Um;Q3}D1&YqK;I~k?hI%<)`WIHtv#D#F=!7QSt=yxXL*Nk)S|ia+5E2*Pqanct zl9&E^#1F?kG<}BwTv$TFAAw2eEtqv1fqU01#woS~~ zSc)=^({i-aZ-T?3{d|K~b_@3`?}H@48 z$gL0_Gb*%sJ^2Dq*{=Xyc9H(IJZ{owi=E>l5R$SMPVbG>^HX*sHPd1)sXwJYn=^1B z^vz-58@6SSYYZ`r-x<7Zv8Ioc=U|}Q14;jK~Jdk?(vO* z7k_)1HsryjI@k2Ccd<&Hjwvx&?_mLx*(G6j&3}COFELTPXt5KzK|(KH59v*0;V$~b zfu%0wEg!N10TM3#ZjaL5efk5USUyH&u5cpW$>!XJkXy#{Q!x*uT?d!|;mT+KcFF!D zZ83*n#T0Q7lHVGlel|r1jXxCkfA|OC8hJS)dGoRw5s(f3w-u_Y(8+r4?uS?x7D*^T zq_+z@hz+<7PA?E~EB;?H2Tm89RR=w1uFG1ZZYB|N*`EZ37j|joO8)r^Uck7MQ(b9< zGf=UJx$LxCEoPAnJTpQ>zf31{oGjZsSjf( zRq$aEw1@_JYw>ez8m8KI&vTAE!0jKwU?3$v;m9DK*ir23Sx^WJYJ&x(r{(SKn|A)@&Yph+m^!JoK`8$Pm zGeODI-v>`8DCnKk*;A0HqtXPTY^U7glrj*|vR(2<7w3wgZ{M|99_!5DWEIWP%!B?$ z0DAuenvqmw*Q-%J@hTIRq9ScfI}7@KF#w#b2T;PfWDFVJDUYUV^w{E8C}_ z2Ul%sI*2cN4D%|OT}u@q9lzR_G1?-=a(Mu%6hg=Cmeu;g31I)+WIlzk-NrjB59GnE3liQBA ze$`R1n>BrJlU=&enlCT=dEdDq1Nz>@GI4D4KTx!ZTPk{VRtX~W{RcM^f8R1{_ylYT z@{EIycH^zd>@B-zjLsNl+m)9_QQlxT{tjZ5sJ`j8wZIk1b_{xTr6=3_^smmCd#!Ov zbgUYQRlri>wO*T-kI|EoBS?S2tI8h0#9fxqd!Oj+o+-L10>cZ000gMAf1-rEw=$~T zoNuH+cx(dM;$uq&xl9O}vahzUybMa)Ekp`rPhQ~18nq=zZcIBrfK;LX!B2rg@H82C zWBvb;_mu%ru3fa}7$72|qO@Qjh;$B(f}nJFNevCs45?BgCC!l14blxtjlfVt_aTOn zk{D{p`wpnbbH4MP@BX;|E`NcU_lfs;_Fj9fwcA*|-av12o~0OobCt2usM2RDQjIs8 z(ZHr{ps2bg3u1Vb)kbMQw{hffaHaIa7xco@glX3|u_-ojDfU^tT{`g%S2w?ModOfe zR29#!L<_%!k5kby|K|M+JJ+*uQJ0kreRn(Iw7rmiUM7z@@tDs_JMO~wLEb}$u?uTl zHVt*l^ZGl!`=m`~P+Jg;tf6cWxyV@X`Y)%ANP%X#g*ORhRVc3zaF!YD|tzQVs8$wyEHAf857?dy970~ zc5TqJ)G89zXh}$}$k?*hq|t2M!`YctIDM75Q<0RkL0QKG-1jMk>5r|#^jS^ z`jO@CcM>X=SXa8oVfl zANK{lOkv{WDKcG$v`16hmk{4lfTXw8@^mA1FJU1;-ws#@V|f(!$4t1y^t|o-PWx`_ z4y*0&qykusj8e#C9rLE^NorV{g9lrwd&B$;p26rUX3#=}puu&2Ozhr>s?V#d}=r0YcEgSL=OwGwnVMP}-I71jPe$2e|wV+O^Qf30CX zA8e1%Nd7u9g_1LJ^L+!Tx@8t6z~5sSW6n-sp%%#%$FNYz+&73j5?}arxinkk*sSPD(O=2+2PDu`T_4>CfEmy z&Ror`0anDKZIn>>iD1O(7xK2jY3)DN-H8HPf&xDdvY??bph745$W>&V%F!I$HSM?B z-LLbI@z(%~F7Gbq+y7^f4G;|h@#pSg76FE>F?()aMOtFfc zK8A_!;rto|54I6^TIN>+`_r{2v!z(ZMTvnd&KhDWtRb+4Tyw}kQBm)16EYd>kE$N6 zQJ8+b38bMmTzTNGvNU0LcE3)NBs3>3N#ksR5;WSl_) z931>FO*cX^2t{sNd28<6Rqt=n-8C{ zK*bSJu$bA^@Fovid?3Z-w{fj$N97s_2hbftM7<8*C+vT%{+O+kCUB#R7H!F@F#!3( zh>zL(2k_|r9qX+na*gvn&0y7fUJafjq-gt&X#dh6INm>S(G~Z{9^q#z-BNZkFqdQ^ z1*+wK5n*ga0vl@3ck$}co_y8C#;|u3T)>5nQ%TNr}iH;o!fNq^Y*h*VvBA>54 zGplke?GS?nZG1t*$CFO>ZpN1dz`A1cHZk~nyqD8NQO`T0GMPGJk;X!=hZk}ss2Fvr zv3ayI)l?ETgdAd;0J%P+X4rMS4s*R2GNw{l?8baom@jL5Loqg9D8}vTP@}5H2L*ZT zt3gagDhT4A8zxD5*JkCCz`dqMXJaAz9M$elWq_7C?kHyQl;7c9j;#?%;Z89`REMP1y)zxy0v7>r%=DYwvf+h<4GYC60xKtF_JX}rY-fU?7FHERi)sbI6w9@V9?=O@A7$|#KG%*SAGLqe7wzVj;M(j5mgQG+5?yZdwbt*8A+S< z{efWg*LvW1yU}4d_(1vi6Pu0O%Z)9K`2;k%7jR#^3Pi+@Z;@*|;v z5|yi2w&Y0`ak~~gYjuo;eVe2#dWn{RaCP$$R6rpNxv-h!-xEd!pt_Ou5oOT7z;93K zLQJ5_1Uo8-0DDn&pA5lA zV{48dXSMb;94x#)k1!UMg6tzXF39psRs{w0ftxAq`_V@R@z&j&!ORPXWlR~){d^me znJnCdIRf=WSg7lEAlC(s6`Ccwyh(RQDf9-DkRtD*W7w8aK_|k`F6banPIntX;29n2h#9_n-43wP;8iPQ6#ept%4cV zErj2uW!ztZu4{{X8*|aNg05THrlu+c43 zQAa(y5l*@`>#`;W0NqicQ5xn9nl7<(Lo8Q1S0mN}S3e^&vxGPUNfe)Y)>(pQ?)z#$ zxT=TF-crFRjt2B%6G>8F8+|gU!+fg9daBH(V>TfohJ(fEPJ^0=$4G)|VPm>{^?Ar8 zR0`n*nb;>@kDuFOj)v64>FXlO-uz{Sr4J=mD5lo^B=1YjinAC=d%$W8!KeYfg(5tD z*p`x(p(1?}3t>j|;bfJkqQx<2hBCYI>|G|zg8D9K{dYVl^1$2{=nYrZm&aZ@J>e>H z#ku)1(AV))5eiG+Tx(E0f>84vqa&8Os%MzaZ>xSM$8=eS{?C$B`n~gR$SAcXwa~nl z;HkhYHT8o8T>+?!R~@}aed?ZPo`qt6Rs~;@vGPU02J}pKcyX!VM?fV{q(J@TnlckW z$@@DP_Uv)t=zV)lT+osG+`$ap`5*78KKNg1S=cxLR0c>kkbrjY$F=%DqMzsN?#=#? zTh)hS#rQwI1Q1HFz9Z5 zxA5~9&L^Nb`tQZqpB$!BS<8RN*?i-c*I-5;RL{a|AYj{Zb#$^zm?iD5dQp9?2rqK? zoa6Bkg3X&nR$4QL)@*?q_^b(VHQ$N!kerEZ&bXt?hwfdu9F9%HlnN-!NsDu40QT6} zU|Z(vlp~&BR8{dg*RE9(#*eUkGT1ksZC`%QZ)%ArJ~d@G!O2Q7Abf_02p!iDyu4Uk zh9xC)lGn4^B2#yVGA=E94mT7t&PXbG_eE}h z$E>l1%b=V9GK@OtgN01Z=X4(%k;Ll}l`OImc8+Px^_GNW*rfA;&b*7TRVcH~L*0|* z1jSP6DPMBZTiAs2Jo_6Hz4t+4hGSnBDG%P$gWhrKLW}SBe~pL50re@O)`U+oxn;>s z{WRmZqKsTl zx^>>NLQb+N6Vvc8cGUAb=W7DaEU(l#Z#a1e>fAyheR=YbMl{SX+2g7pm#j_5;db^c zZFENConHe%(Y-@R6*v3Bq2*nJC^o6m?6($^+6-t`7`vqLDglsHRb?VR`7Xi7-`miA ztl+t?@v3^mrO|&(=9@DvL%l|?;{Lt-gdzGOKMJ>9p6Q~8@#@s9S1th9q9CV}sc!lS zN53jNTXp*kIJhm{D<2yL>s2xe*+=^?mLU3jErl+TDtHM-Sf%J}y6C2JZ&WI+A1abs zscnUUdSnrC9youYQ#1ih)f_5}J4tkJkSVppYZeAu^R@!=jg8VSa*VTl$Gf1T#^&a) zHPl0ME-H2U@FYILNCMzV4TKm)S2g<4T2+<2;kg82gTmC4gL^|nj-(UC@)91v#@x<} zu*)zDWCuf_tv=jLGMg_p_J0Fj^uio%N?u3ealJ9-eQ)9iPFlEQ&1RtLY2KacHy(BD41ot~LMDFjBoyy&hUYGeIT z=FmWRtE0Fv$*y+2?8atw!al3WYC`vnWj6w1E8oJ>*RWJfC=xcY<&uPQNn#D; z7o?{w)$_FmfAUv4yGFp^XpS`6a0-jcWQ3wHc8*MJsV`^6t2pVn1ZS2fTPG`M%D=qV z-9!A8cR!Pi&sp-=TniVnMGm|L+G(9kEY^x5$X&}AV(kV_jS1N?nTK8*H7Rt#;(To+ zsVQjO5XiFO?3*XFJgSf7isAV2qIH>VP?LAIdYO<$#7G5-=8@C2UEKlU{lla)Ua4;D8N7JwCBw>j2KZr0i`$Ptb1-4T9QpES%EU z&9k+SH&)0^A4PD*)EHlm>=TS03T;T8F(XL=U^)H==e9S>N^j9UIqdxu!%Yjk5a7Sk z?Q(%CmsZ>M*S@r&hJzN>$evj2E0L8;SK72ODq(279IG+Srf4*=f@iyt?&NS*FZdP> zUEK+N@&Fy;@_mFgwi?Mi@n?`?KY?mPUn_tqdK4K|qre-I3v{Oer#C<~?n$YT>#$~$ z53gx&1jBbl$HhF$F5wBAt))+{*!vRDKkG=Yd~LYtNCDkb)bxZqGrf6|Wtacz`0?CG z4fQ3%xM_|Xhr6$+pc<&dlCXJu(N} zaXCnEB5z7};ZcoAbb~`$s6bMA zX$}5*pACpQ&;457S$W$DkmAR_guAy5a1}A%H7+{RTwrA@#?q6ZMleSE&frYLcaBNL zI8keAtukVMPLWBylBY_Mc&NsHlBFxh9oG#y*N1wGIOlQB|4STF_PTzvgJC-lRzA;> zSqO2=QQWy~V|P1!eXUOuc21mwqcyI*ogDZY+>nh!(IW9m`5(te#jd=bB#4xi6bm^2 z_8SEdE57lx0{uD+#sO_0jsKn8{0D^fZ(OcFi)~m{_P>1S&(as5PXE6uP6BMN2;LXE z?>m6TEWi(6)~Oig&*CNUdz3epxN?ag^?&oM{xxjx6FWOoNEFQU2%Fy2LjHZ}Yne!Yp9Oq3YTl?m+ z;~SCgP6OG@&J`YBNe@s^}v9HY*J&(0)9=8?taOeV%pzMPl9K< zH!Y+SC7EnU6Q$WVr@23rc_>VjrF1q*0GQ$+%9O=+z~GftdZ^dv>de%F{0r)*AV2nH z%s4JFNZloBpsRw4SYgr~U!ZczOgpae!)IkAz!Upmnefq#Yts9uIg@f)1sfo0vlGhN zp|eqvr9H4poAHRyhmv&2#FdN7J&}<%e4!*tLd?^ci0$b1{#@Z0;esMrUp|R=^It#= zV)9G%4l|NmPBV7f^~pMkz7Ol=>P%h5h{KnVG5JF?k(sr2baCq~{py@y(bnpS>O)ke z_2oWHA3+X!|M}Sq7pK8qf}a^30IPYzzGlP~Ign=Ygn`yZFM$@hB(CE+N>u`&5|SiZ zlN)N2mdpAI+15>LYX;ZIFP!nraEBuLcK}uiLfGs1h7J%SkvF%xWY}*IR-iphm=w5^ zorF@qHakVXO=eK`Y8SJ`NxL}DGmrU* zX_L$+Yfsu=?Czm;#PrL`X_^>V-8(xfPf<-`=^W$P;%{eIEN1^6L9(DOxgK$-SyqA?J%_( zc+;#;XKoA!c%*p4=n2{2P<6+u1f-t_gnG2?I9JirROfho)-GW2q0Dt+5NLyt5a{$Q zJWgD6@YKK~MfMgYKr<2>cI3ljMMnuvZUuWQ9+m*{b8xePd$q-TgJiEitg<8?_>Mj* zu-W8#_Tl)+hP`SOi-RXEmH4si5jL;tui^aRy4PZ6nmBD?E^=%PA2z8i?e-A zR^EimwP{;dKUY-0D=0w=X(vjfvuf>P7yq+BB@!$nosHK&CCFM4S)@0RB&J7ll<225 zEXF*AtKr(O<{;WxL_ow+KN>h`h6r4@|0pEa9Tv?XqZP^4*{O}Y&u1Nu8#s)sKn+1^ z4|8&NST&DTD5GZ8<5Y9C$cs#!r@Zn79j8kw9&K|4j#sTVJJQeEsboYfNr#7QSPTN- zYrnl;kz0LQXgktx=@F0W2dcoyxkO0LwZE^gE3i$)h`p?n=eI#KVm zWrg0%!s9u1&ZjyiMCpBWpFyK`FDAVYF|qn4wcJ(uH;5wz5@R7-ZR^oWGHm7&OVOz5 zKI=f&muqM_BN6ew^kEk&9nr*E_94<5=UcVJD8_!mL6Q9aG}|<0?t87v5vVS0gC>lz0pIGg)Ax%XQ-5;6wqWn6K=w%)z zuee7@+-{9eW?|Cv;XW%ORTo1OV>i~Ef3J}}+$_%`veqGSBtC4H;SX4@Z5K<_tD;rH z!jmKfKjF#nj^-wjxu_5g!!K%;BkR2lZuw8f{g)Iwl5@=X7GtAg+!BDh7P5@*n2Sa3 z=xQ3e;=J!pr1ck*P-92EC~#a1UU+RjcdPqZr-TyFQiW~zdH68BLfgj(^ua$A4^GE8 z&Ankok|BJqwfRbFzOUkKr5S#4gM-^lS2E&XN7BA0lzG4J2P1Bdb~p1tg?_KR4}!VZ zVxI1kvjJ&@d*eu^#q+b!@O!yAw&k?koGNrVfEaIw?w~HntbWt>-oPlO6gq5aA+MBz zYD}(Y5CRPDUR2nCM9nA6B=6g{S1NiSCSlq^k|kvo!;4E1ra=;I%Atb#b(mQbU^{aT z?M-}XS<`UWTC17kcps_&uba(lydISn`{J@nG)vgpVd<8|b%1M$T*Pi=@4upTDG(A+ zx)g}AW4Bsgr*tj$DnDfGs<8WEYZaxsHir-dhV%6=ObpxtDqj&1W}?trpO3kVaq5x~l90-8gADie!I?zj=j!daCnMDFQ}!nw5MH^!!VN9}LKc>E_vs;yZ? z^$}}1F9pv60gCuhst9b|FXhS2?Y?z6$wJ^z=afPK5W< z0~Ew-gCWJ|s{h8Z`!l2i3f5fLx36D#GT5YUmIZhN4L#g0`cb*~HPZYSuKu4SayIZk zk?el`Jys=fo2&BZ8mat$S1bJbz5j0`-2YLZzcvr_uA!a(c$g^R#hY;|FKadf7pa+q z%4AEm8j@jw#5Uq=^cqPBB*h&z0Qhs@!>;Cxv#7t1%zM}NVIX?cF%x}eC$pX<$sWa^6k3G6vBR8S`d%bwi^eAOyH`3jbXeFt{ zv}uaObfQRcijR>AMn=L&=%*Gv*&5@FV?(kVJA<%)XMYPag09^aqDyLwD0}@<7N{lv zO2p7Gy|eXQDmS?6>EuY9x+Sy$G|JO>o*ScAqUo;`_pO8LB_Odi0LRF*gZjztS}Q%2 zTf~VF5&zHx=f1~9YLY+?y}~EHoo}XCHoBCa?``Ia&E6?>Y)rz!eG;d`ii@POtW2^h zNj&}>2vYrbk5_G4>|Sot6v3+lBSy5#2>~>ofPR$DW|`_U>f&^TjKa=lF=t{VNcfu# zNidhqxN*&ZkK3Ak1KK&7TRTfWA@*>9jjTzenTSHU z9-KjUjdvX*wicXrc$;Ql33UwyUOVYxX{~?aXA?W<5Z3@Y<@hY77L?Kp=%XUhopbjicvDpJiR5!{*uOVuFYl+Omc^ZsF^v1Cz%^dcgM+x>SvsW zQKNvy&o|WG8KmYzgrP5S0Q@i|_KZqktv$(!{WN8pX%6=0t_i4kQzymXV7a`-bn9=J z%~S;;gQQ!=Mz*l5tW|+O(HAnVu!*-d80F{>u2{;`!2(S0^&@NLn=m#ifGJqQnB;z* z!)zixulhAwz=Ov&A>QO;z5&L?v&;%%oV;nWabQK61IW08cO#tqLheTi4-hLRChmYS zNnwl#ci6k(O~lsL@8TF0L7;FeJg(c)L$|FV@<*MW>$D?#qsP^_>i~SW+q;*>_Vh$5 zlL?yExqF^Gd4?BA=f@zmez_y4wmv1Q;+2aJO54_+sjS|cWx;LiA$x<0X)@(!2xKYv z2hMc7VFlg$>EEp?E(chW;hdH{4Tf*UmO_XxMx@ zw&L2dw)L|J7AEa?!%dJnADSWug^0{zVjf;6@sKty%Lc#p4t)a@L`m-4P|cqKY2<@w*+1j!L7~HvNWKc)^Kd~jxV@gPQz;L;GD!mQ zT%_z=u<(Bmx!I4{$Y*RQn-bucu-N(gZez!ZGHeQVA>HWhz909@>Gt4EY8ghF4%rmT zo7p!;c>8{_ht-wUtitL(#*8`1&|o_8A@+G`rwN7yFt z6iVbU=G6jN=w}ihNP`g35Ekxx8O*Hp;_gGr-AcA`lI0c3N@8PVnbBp{uv& z%v_+84(^SkkOD&a^yR1d!lVvw0Ep;jXIl$@amfVIi|qIA;O;nd|DehMP%9v7*-t_?Ptn zKQ20*3tz>aD{D-8jL8yTm(X6!lmeNUGgyT6rSLqeN+<_Lu(A%g$c2L2~ zZpDOj2o@0p{+k}w>ccDina#3E70ray!Wqp{ zsaYk_z$FKV)T;^CbHTLFlZ<6qMTZ(w<{>i1jTk3~-3zkH#m7u#X#wjAiO*X;m8?}| z!o1M>qvM;f9-)}sBBK?)9JM8!^IaOzh} zlMHPa2DN}1S+C5OiT4OP&6&@=1f_1mK-EWHzG+a=1mgYM!_ zkbdeU$!7eywQAXgYGAjdwqZ+4SDE_74SPW$A~40c-Mo0vYUdo^(PL(QFj>daCh{XB zZEGvyVR>M%ir*Eh+(d{BJRSgBrKD^j8t(s~2NvnQmhB?wa{N7;wFbMP8QT0#Ux62_ z|0aGQcj)U^Sw6@|e=j`SbBt@99>lLczxo$z4$~LLx>3=3yNTaRR{G%oFJMt@!v(

w*8^0u{8~=7=L#+qHp7>gRlkH8+%c*ByE+Ze?>U!~~e7 zvL=9sjy|U7c^y<9-hH+wdU@J$zOjKkps-n>kr}mm9iZUUe@nI=W@|6v3^|`lSiV^! zLz{E=OA$70r$3r0=OP(3tk}C2*F|UP?!T~0d($Yc>o&Y)zfS7nnpahIoPyJ%B$-Bl?dce+)FxPaJMNSvd#Pd-dj5EYuQ7vF* zcCMh#aDW8s>^cIJujfHNK+SWW=k{aFzHz1P0D<5@R$p$sKZ_@*W0t6=))|!|wAm?S zqkS@({pAN-$qdiGLEs5D zh^(LLw+r&{IAhRBT*kJjvTy;2%X0IeciGg-!td(a9}9`i!ZW^AggFT0TOwkkg|E*g z_ulejk>ZddvT_Z&y69_LVKe!5%g1t)E#g+p+F~|miZ30nhp6;scvlV`Q&dqN{=PD{ zGs-DK2_z;t#^0QPmhoGiR&y`CGTsF!Wz<0SW}VX*g*NKkoT&iz+XULFmQwHwi<2e7 zU(sMuWA64)yfw0EDc@@9s=|M#Qt>t&`1Qa)% zuHTIEH}Mz@{8O%oHL=5r`2QZim6(FDUla(q@Qo$jqsvRZxONPaoN1Xs8;-vhP}9-{+Qw6e zSm&GO@54+@jx^_W*p+K6OQ&8k#i32(kAd#-FQ~*7NtKkTk}7f&qeD!#b0(P+({Ew% zWx&Ys%%ca*Y$(r5JMq^4I^n87D#U15;kK@7%`1k5YbMFGH|>?$n;8)69wuygcrvlk z6YdfKvU+4@Wp_V+*QX7=Xy}hMC?h@-OQ+kf0% z9yL`r;fdFZCac~eZR{4<{Va{#yND?N)ESwYk!{k)@C^Q?@y@9NJp z@$fI(Bk0Mt716Yl!v1}E0r;Ixov+0n9&@aLs=G=Ga^y^UR6+{w^da#q5|=w8<3^17cby7DNbZ89(@jzW9o2lPE>!Xt~ei-GXgluC=Tj@v&Wm7Tumrgb8|G}O)8UFk}|8&2iK#R1BnG42waDFs~rSx)9+|o3#L?H$L0C9Er%;whp3;VbPj5t zU_Wnst$C4wxXqH?-2z{4Dnul6GWnUy)xjmPu~7Iq8A#R9S_{1rXcOg-+T z*y34-%jMO2N!&w{2Zc4shI6)<5XWn>!(wVe@SHK)J|V3#bw8m zI+5{h#YgEJC9+fYo#7-B{fshv&Kb4_J;)Ys&C@G{rJZNPHa5gi(0|=F&-sWteE6GM zhnD8~y@mWF}KaepvrttbL8|2io<)Mu##=Vm0pw1=al9#7_5ER4*toLfomI$dv8Yo#JE7#QN zW+wMoVU^f|8fCnf2EYpUMR4b{Fe#VV`2&&Kd;i@D=@o&D;k5FHa`pnRnZB%4gcQ&X zIAIH0Zhf&tZS2XQ5gu7#Mh5@d-0>pS2C`}eFgnChQm^XzV2ZiSedNr!Kw)DBS^*!}tUv z;?3}P8O6^P21>2i{U+08S#+u={|6_JpQjt(K=waF3&02c%w_!_3b#M5Z9wmO1Gtrc z`zI9g=du7E71+Ds%-8OXGVy6~x|f#*c=CIIXCSsljD7Gk3zGi|F8=M~09VuA3;$W~ z|MBwP%{ECDP7o?gZ(I~qzZ_G#$6viTU4T6Gr~juY^yjDJh+vhS!8%pi9&09QCU+&* zhyZ8yFyC}npdp?kPX?J~`6qa{Oy0(ZSripuijT(qPm<)1+4&`?$@H6sBa}ur4u69) z+Ai`Mny(}*C$2AlH>^W{WVSnryiIanyLv)`0RGLycVYo9WX+H4C-xb`JJzx zd7o49RC}G)tGB@%xb7)sNZ!3 zbZIxtIyAdtGjed8I6-!4TH;Ln`*X+(IunsKokL=#uf>618zVuQdvYGniN0$q( z%Jjo!^EOsUZzGCE!3P3_M+5Bm)D-dXd@2%9Kv`F}iX?wlCafQgjFagQ5-P}yC?TE+ z*+_S95$H6drOTnx;MJJeSTy7B(N?CVN&k0YT9A2HuVp z#Bu0&BL>pBK+f#4p8K?8HqAu2N@$?=0Qj*TWjjlc#t*YxwhGax2h8yfj`c5+I$(2Z za&{Tx(l!*1ThjZvn4HO?p~F9trs4M!eWdo#RRfW&i7-i{w%vqz-Q|`%Yla?7z18Q9 znEIOI#P4QzKVCl5DP)`y;Kset>PQZF6~`cBpRxND`}NnU=Jm6lw?NLscFe~bJCsXP z4rjX}6pzKyjX+Y(&Ggm=tXZW8059Okgryd<=5T?_lGMli%Y2Vw|~4|B|7x3L-zvmRA#-e z0N6&k+3Mmzw>Yu}RuOE!v13OaC)IO(;Wze|mMeBjv~46`^Yz;AfKe$BpL(6h>7FG& zlLKM}TeBoi9G0iNy2e^!iOJ;`3D?qrQJ)E!{~WdPl}9iiiVfD;l*DJ8p(}j3&7F2v z(rV>!*w|R;C$=sEQ4Wee+mEbI+3awm<%_2HaD4PEN2#SWS8Gp&30(xbKfjEW#dTohdfybqV`Kwh_V^GMz>qh{UY+&6+!-JqUG>J_AdV zzY-4dS`dEb2wSC~wCgEfH6wmIdRB?@fD-AMI;R*rLHsnQ%iSt5A}P(CQL6E6rQ`Ky z4VjnZsNZhmHe+2K0hhiU14_jzD86Z3oGmg?FSFE>sau0x89I|ok+jBp|oqz+Bm-WL4yzS)DaRRP;UySel;1gg`F$SjQ)8msE5dpVP?&i8Z1QM<&kxqY0; zHz<>RXXn$BxqUs31@UA~FEIB}Lq4R-1sbQ>d$b1V#{f1TTNUWR7>l)}@%hJ?45OJ* zk+zy*36K$e!7aIlfUJ*(`|g%^KM^bP)*MFqK?f#2)}rv{6Ys=5c^8ACqm=4+QR$B* znF~CSuc+C++Get+vc^ zvGy1&oDxxngY{eZdE{z7YX1cFU^g3`HzR||lZTfU5u`zVh|2iqZGG%DX~$&ZB~N&6 zjSrVdSALW$*_?+>xY8IDX&iX{_O4`J?k%bh(4+T{l)>=_TAns0!x1urWlXRMQz-PK z^qYeVp!!7x%|0KO{!JHIa{Pl>$NS}*6>J+T6h&@S6)f{AjNJ(?LLBkFqpLIi0DdF4 zXi#6j;jz8Dt@Kdl-3Zv4k9b>LQ&)-A#QLP z6h=9&TV+3LVX1D@*T?wP;YRjUs}lT8FToVnmm^C#hD+O~;l*aF2}FE5qBeO!YZ8p` zY)hplj@I>Rb!_Q?qj#dCva&q9BS=X3utzC@&{TrkC9+}!X=$4YbnHkHWhDLx2HG7U z0AXz1ZAo;JeR*dfu@_o7AiR5H9d4d-T9FGda=;txhs)rP!{8D*4c>xnLx!Fx_*)#d zyWeBLGbQdry zI1~l7Y_jQyFZ;zKlao1uvpV%BS0?KcJQ#`|3#i&G<9-lWox##n<;*nK^6Lv+4`$K! zd;9IHJ$_cS16h|qi(S0$_7+3mWWe6h5g(V-U`EAjFR_#plA2;iOWYGH`CJ%o4~rCT zkec)??~VzZhK(FrB$irN`P>|aDm_B?%8#$Ri5h9)p>``Swi;8j59E4%7mNSIo!;Px zEDV}p9H*aPnO(LK%j2{*uyuh?cgT@!X{tI{V z?{ONKJJfL_FrSavq1HKMV5doI?x#D?%!0^SFs%rol`&Y2O-KkKM} z45Ih(SLG%^sGiO*-9wdizak`2pdf_yv2Dq*?8=JK@l88ROT~bfzx7pwl7^urQ)}!fK$lF469i`B_kR+1|6E-HpCC5J zIJ7PDe#kISq!M~Y`}=_D(nO}fL9 zn>xIGW3D`y?y1NMRP*Vtk#`A;Q6Xe{ltQnnyf|(^V^1t z5$)~5^cfLgOjTl|4lSKmiZ zPwOU1g}W8X_Anu1f=an{wV9N#+*sN;@6O$ml_`EGTyDq%qS_UAyPkGEz6{Wn^JY0x zad)4eSyT&uu7bXNn`HBYV6hGj5XqF^jRAO={0IH}OUl_1WjR=O?9Z)oXFhJ!M3-lg zt3-Te4k^*BIbSN*q1~)XqYwGo-qX^v&a~uClyOZrvYm-#y2w5#Fa!^I$9Oxe$Eu~N zv?KzX9)iJ6z$O2_4AbHh_Yv^L9?x0@)WJ|*Rl+5uB1sPt({WGp@1tz5Q?;qr)eB#9 zT~k(UsOO8J+0{-=(t6GU*YEv<9tza(Jdy|ZCU%&mk z@;in^9KN_+`6sl7?IdcKl0h8tZ)pb|!ncv1oBMa}W8bbWc>e47;S0cP!xllU$6fwm zqkq(Bs&6}@wU9d8-dplu$h}v8Ou(wKUBb0VPG7?592>H2NrNf^=nR&`<1NCiX zdw(im|6zWw`oipLlZrJE&&X0~mwlIpF3KXjy-^ z`B+D38@tYexcw2$EBh>6qMTRLH=@7P7x4hpvmIVQufF3kQ*jO{rz~l>q&&*(TL+on zj7j0Mx`$<#YPg$u19m~DqS(JohGQ2a?5+Q80Vj5RN3n?8T8xXWD%_Z2yoUrJ{0h!s zsEO%~5w^N}@Fp5iVwQJv62-#-AuDG5}jE zap;Ff!}9DRRrP zJ|fvxkRJeJ&b3$XFd18Ye!i@0%~HFTJN7vsO{JRy{4Fe`-2;Y9n&#EZ>Cu?@y1x$T zh9O^0dN>hvdAhBA@k?(E5+w7J^^fw}4CEvSA~aM6868o56*yL+Fh zBVtMAY!Tk2}-;QRA6`9WbzeiK1e3#(?qC=uz96NlwEah)O$uG zLo9sg^XN0j-HZ-mYchy31EI-XSa+i58?%rsfDAi!;Gp!pKeE$MF5Ld<9d4nxaYQ?rVw5Y71NR@&IQKn1 zu+#8ZB{MC`c6z@_Fb{pWHm7;SlIX8|D|8i9NLbtE4sk&ZvSN5jp&6|$U)uOqq3Xgsy88Ft0N&XUoPi_&)#gEzw9oX zt0>W-BAatVEkn|G?MOSncy%TQPgS>Oy#~4P@#~!KQL>WfL)!!uY9t@K4r8e4l)h)` z@j$v{=q>Yt%UzP4buTDeV7g2ANi#=$D{(Sa?3JxI2P(Pk6Ibuu>`YTl6-qRd%5m&w ztc^p(mPJK#T60Cj;g8Y#fTQDbB7@cC^h@&e0+f6$<}H(YRAU8@MtrJv=g68O!A|yG z99p@iQWQOUK^+Gl>L2^=zF4Y)r+uUCx#qJt_AW+hA=!DAo#jN^a$_>R_nCrzb1!)w zTj|XTU!@UReK*HSQ67_RB6<7U$`UBYI za&WtCTGtaEmTw0{hjDKrq{oNWJqZfwGrAL1*#%gQB*ajKg-+L(v`=nq2V?}7m*$%= zT56pWN~pmbvw&_f((oEqm|H);V>lQ<=L!j!cho+9Sys25@NB zyd1Ry>cv=96%7qs8gYghLngUun;cex*ip zFShL6ugjB~t&813v|OdX$(iK&8aOgGxxLwnR4M*S$9>s^W~|%RVCK$styGwSbc>!*uU43X&A_0vNos~j+MVUP z^7P|DX>(@NtZQ)^<`xf5gf(y-cd!p?3eLGJLalIfkSQbz^6rSwhAGGE=Uy8l>l?84 zpTEF<0r;=Cz0YonIR+XUCmA$$Ye_BXb)_%7ak`uW&8QHMQy^$w#}GEoxie6e^I{*L z7d+JcyU80>Vv$RCeXg-nJ?F@&HiaPIidreKhD~iN?UOJ3YuO@>-Url2UslBu%&x3* zAKe~y?L-`c9#nK}h3)PH7(P$lKwH80?>eqF3I zH=^z`IpjMm$9i;vNXHvO%RiV+;^*7!Ks3;Hko##k>JlO&7gRV}jCJDtOYaeElANq% zCp`9h%pv9XN(j+}cUOp#0qp~b(@wxOs>_WxH2hfY=jD7Ehv7 zMj5#gs65v!yt%aVV1~t;L|wmcM*AgtJFBRG8XT>iA5XBVj-XP20&=b98A#BJ@^??jc5@TXa}=(8t08_N)+qSa7`$ z%GFukIEv)o)z!E+BQ~15-pxf9U72C#9a7C5=C4T|5XU{mLRGw++0lQCj;`S1U)QVy zl4om=Pfwc;`TS#F5K`@sC<&X&sbZTW7mqS#9f&Xs8v&wS|EO! z4w8ZH{oNq;QkeQp)de0<9j+cz6gKy& zh{MM(1=R?B;Rpj3zs~6Gbpo<*f0=$*5b-KXZ*DA)`^fkLlz*{UWT=y1K@&vXQ9Tw< zS6+>y?{t(j09VE7?q*ZNu@Eoy`%=;(6hq6ohA)!dzsZdLqQznedAc-rU5a9nB-CY0 zJuy>4>&>ywJI)`cKc9VujG6XPSN_iB!nY)WbG`?egpZQ5Rpq$Sh829O*f=Qa!PfQZ z@5S5jHec1bzieP{qrz>~z%FAfRFKR6pyEnil*XuU3R^+(`tgU5v2#j_%&7&f0yPf{ zug5O6CoT8}T9_9Hmwh%x;Mbk^NcWAIM}~o)Q*p~U>E-8rUpijaEYs!l3+KEFKS%U1 z6csPG^CiJ?ke|xabMixrlLD5d&bzIuK<)1jW#w6ChsIlM$1${jK z@j^l*v|j&pWS`&HP`kVC*kSl=B?l>B3(M1S`@d)RevHCl!i>FSyqff$uU{j!;nO8e zE;)9?;XdUyM?%H=;h?nPNTjb_Aqn|Q!O;EJg>_C$^TlfJ*u%{assXcYDQjy&7o$UJ zKHYpOlQ$9h;&mCtjoPm%kw z`Nb=y{|{|%85ZT*eh+U0QBgpVE)kXPZY4#!8$@L2b`X%*Dj?l8q;w7lLk$d|2t&=# zFf>ww(%tdD2lswzKl}N8c#rou_&CGdSDn{7*IMU!4`Boro=%P2gV&aeR&_l~c;ZzO z6@G^TaxKh;N00|}hv+;#7vcc5;tZFn`Dv%7*i`}-WIiLcQ_f276JHX3dVHg9;F|e? zar~nML4*8Eo5}Lu{&lBUk%mZEu2N4i^V{nNu-HGIAGSkAQRVZyww9li*&;B%x5lao zL2nz`W$7R{ zRq`gpN6wsWq9iL`bfM)ek~_JbX4rq!<#9VhEuOZ?cPU`COq*B|&FI+o()^?>pVHgR zJhDtjORSDTZ|aTn>ky`rSXTV(+)A_=b1+5*J80f~zsFYgvBDOdh$&n8-cJU;zQ$Lk zY6vg3sJPy55m1uiWv_s}at<~!zb}RznK=#dd|L8oD)mkYb?edZo{FU$F)t!+1SQwZ zCacs9vb9~zkk8e^O53LI-b2{($_~-nD$5agKc;hWm&~U$Y2Z$SM)Boyd38CoZt@M0 z7}^+4YK=?1m}_@tmElG}^P%4!0%v}u1foN(R(|IO`Jt`WcaB%jCDFykGmjY>Y?up5 zHF6^3U5f0GTZu0+k)X^k3)z*SHRfFf`FOZ9(X#bykV_{btF)AkwDsCUjbYYs%rtb{ z{21?Wq0($g*wB$Yk^S9{J_B_&MUQJqb(8ZBO4Uwvi*l+giSY!Ej{$Grn&M~aBsOj` zVDh$QTtW+3o+-{TEk1ZN!9?_?WUwvTlI{+6S4olGK{ef?U-aK1yAh+6x|V#^@`*dE z_uZRyXWd{g(lVa>^t)EiRqxA;DDoLONE;HoEhny*6X2(Oq>qYvgL01Gg(W=jV~=<~ zg%@kW*ME(0U6l5*9}}viKY`M6_h?)!KR4Wjug|w|&Klkk^9%^kipg-@2pfE)W3A9< z*;}g{zOCTd?e}aJrJJ@(Im9`I+qbr#C)HWgfm3CDL{{P95v zf0E_Y7A#1rRe)BG`fAE=DQ25&X~K(w=B)z8l4AnN;=O`nHS-L(DEv@&P5V@Hfw||W z!>GydNMJ5P-h)WzaM!kJgh$eD;jz*K3Eg%>_*&q>0a>0r-kjviS6?@L0su)bkaQ&be2t~#BF@=)#2@ah8rVU;dHM4JSw!?2LboJk5ACoBzWgMPn_$o1I zPv$mWHmA=~6RLC(Q>yNUNu77`@^Eq#Y1{+xzVc{#84=}nH^M~Z1m4MJCGn>H;0DPO zv0Ey+UbPAa0b~~UOVoZ(5bWoYGakj3FWb&o^s|2QOMLJTD92oscrg}1*gWb9aI*c0 z=>c5W?3*U^uZzkOvh)bIgsBh{diN;}a*7JtPmz_*(m_ORo#N(A-PAklnmLd`<6cz; ze%L*^-I)Iyt^5gK*(omrftAVsz_|oP0sz;i=bUo?fX256+{}2etJ5G9(b^$mZan;O z_Jhtt+lXI4wuAV8V8f*J#xjRjwA_^+{LdqIQ$_0ZY03JV@0p16F1pT!>*}Y6Vt-6b zJv{eYE`0_ZqIMpbJE1U1Wd$(~zea670iHpwD;AW%%Z1pG-Yxu?-l!*3AtJsVz#6O0 zh+09;Coayv=t&(YLQQc!5tjHr>=FRI=uFi_$6%Qibe+ zOC~jsIaklYty@X7*HaPlNvf%^!x}RN&h6^`f|rtvKQ&bEP?kL=|M>LaVs<}4as#jX z$ZBZU2U5UD#=1Dl4cQmwHgw`#3;Ja_y3sZLbo3&f`o&nDX^8viM`(Ws?p-;|WCB&(@ zwKRn_av-TvSBa|TAA6D1Yj4ylH04s@9`OE#5@`~{P)O^mY)V=4s|`1}De(1Y)77YYpW^yV6)^;kHK}G<-XY^TF>$qOt8^4WUX64gKb*u4moDxtpmuVL2KnP z&3_$|%J!?_j)2^5Idlo_(!sE*{r%86uWPrN-a9u(Wuyex@Q1D$i%<$0bfWzroD!o) zmdsnFT`A8ZJGN8PewTrnD)Pa{S^o(&_e}MnsOQO}12S$NE3-7?ZpYY-LE8Oq-C{bo z1@QLKx~!VWAI%cFGf4yMtA+lOOJtpWg90>qyDmI*r=IfF)U&iShc3*vdT~O!1-o4t zkyPfTrP!_r_%mCNricu3HA3FSH!Fp*c*%C7yz|}ped4JvS8azNWFa|`x#$iz<+11g zGP-)_+q@M~{4gKIBm=R*E~lvt?W-QK^pEe0WOmMdr+FO3N_39@>BMb8$a~hfe6y^Z zbn)9yPWCGmuuWqTlAk^nj*q(Tq$Jg-;}{BVzUAM|cQa6eUp7iRP-LfLeF~=2p|xKn z`X(+fG`xNxbOY>trv^NY+NL2#M+^%cmemB*{$}tFv6qtQxC|)+*k%O1pB6CMjeJD+_ zB1E|~RB899SD8KiGkL)x!?U!|v0o!9+%cvXaB3Auc?}gL4#8@$6-27DLdUp-)%faN z{>;QzngtHdAq^+WyXL=}{_uqM8J^46Yw?XybltdGqFb!1&+2UgBJ&fguWo{HkR3R0 z-rJ8K`EBsUSIvF_Gux~CmdkV1(^!kO@R;IicZly~Cwyh;Rksk6G?=o)!Uy^)HMOo;=(U>aRHa`4MJq=+I06u0d~G zo@s28B?|*^Ez)rHNEn=7C7o1Z+S-qu7kLaFh(SXCk%zaEjCtCfQm@u_wko9vztM5M ze2E*H=`z$zGmtX6wQkc!Ven!xttc?L^6fK!Qi6(#TGp9wKz~mUA=dS(VbPztx0h#> zi*Iw4hFehUB|P$SfaoQy??}^w2^NBCVr>mGUw&XPeHZKDBdsxB223~59NT*xJNBS< z7=-Fh@+?m)DvZs#uaah!q=!o{Y#%KRK&s;mKcXs%aY>ks;+*cEu=&@${+pF?n&<6E znLGj5+?lHGOWecNeZDSG26~za88A&@{kB}iZo__ByRcigMdTj<(~*a8B0yEOV=4)u zALg#ut(@?e*L7?i9;$NeXw;}jv^;L3)H;LJzzmUf@ttN|{TiwNTBs#ybqeE3!vQoBLe0v0*&bY>D6Y{yTCo=wVy?dlvM{&l%r_ zXTpJvj$xHXxi2?W*hdIRj)JlZ99>FKS}Pb&JH^xDf5%YLbz$rjRv^qr+=+GkxDk@L z4_$8#_#0@zE?|71Guzd3h}iH3$0QeD(0HRsolT6=BF_)Bb#MOt1^mKz`h;WkTxM@W z4mkn#Aq`H>zWDb4A^`(`!nta~ORfaEN1f19{#I_by9}LOC(1v0M-``o)sxUsIt8CA z|6h29U0~%LGK5-bB8;#QcW>-`sBKMLW8iC`C6zBF^uKsTPUFR%3?ZRuZy4PB(3LcX z#R@cS3qNH7AmX=N7T+~Hr1**A@|x+SV0pwxzx>XJgO16gv-vCJ8?wWSzW|@ryRzgzB5GG)?{+SlMT11twpQu+N8D3o(QRPr5>(ZL zwbo_bRc>CTi`?6;tbdV#V}c$==#xsgrsuFiZtiRFyX40S+frXRL6>p1moZ;RjDl-~ zn`|k`GI^vvrCX=u<6q3%@y}lf-#_e0ebcjK4bUD%XmckG#ylVE++_Ul%aVS}>WzLo z0%gS)sv)f`6;XQ++H1J^EwG!ZIvC37NxzOCrX6MFGX3eFe2*TUFH8eFf@ro$|fA-zt@e z1?YJoWVEv~E7jfH8nsd`;(iUMKfGN$GGC+xzhk*Up1G41*8{xf+f!Mm8-|Y~0Cu2@ zb>o)yXOln6e>QAU)tB#}>{)Mby-s1}wF2hVo-m2mK^fURVDZ|c>F6*p(10#~kvK{$ z^6KUW-iD6Q;G$MuL#7Y3d&|;_^`^;*8oc&WdIqSP=OB%``>z6-{c-WQO<|= z4OKEH%ngGt)?JWUaW6DTL+Qy1z6VqEliwrIJ49FP-*%14lkKQ6J?M1P8*3exz;iUq zf3D|kWE$9nqDp<-!+O3v?^zRd4YL=qP_;UnqLaA$9w3mFn(O#~I1Ep6R!mwR3#!c* zi(tp<)Pb>$pt_eAGt>u*w}Jdmj3)4w?WjKne10H6&A_M!P8+zF~jz&*iX3*=L|@ z{~Tw$q=oe@J3g2Po+o&Mu&1S4Z0WMr`_?i8S&L8&$}(P^uUVvTeY(z#)ZosEi->?; zF+G#~ki{+J?2Rj|Jwr$sx5ifLF%4bf&m}vnA8vacii z*zErM)m?Kv3=4G+u=T1Zc6&VF+l3-sTp5guqNc}lC+aeykD+2@xNO=%Dh<%;UXcN> zqkAXeP_Td2|D+yxx#mk}z=&@* z53M51rmu8>m!H=}ED{+MxTyKldEy@{wRJPgC)Qv;OU?Kwo`r- zQDbI%H5GK$XDYKgLO>4pU8ZP%w zx;t&`L7-huQICf?cb{yvVjg`*#q3$NULb$j@?sxkKA=g*Lm~Tppl}A%voA29F z3bHj4V?J&d&*k>tT@}h=9natEkEK>b{vwk9I5?w822u5t7^;s^4A%P?&KNQGycZ6j zVOIpzSt#`QJ~}Dymdj>4!wSUaO+#XZwf20DBsC=65^C4 z!T2Z~ig(ynx(a~nUn2x+7qco$lTdZ{2KE^WifEUQ~BGq>k$J>v<;QMOHqmQEK z*j_=eVsrTr_rF;i1n%3tT_}|Gx0tt)Ea$uC@q+FyIKpkF6RdeU3%&Dv zOgmj>fUHdt$Fq#9Edt6KBp!k%lH&SrfeO$S+uE8(W+y=DsQ1nMWCR8_H=}*kc-|iD z2{#5mZPXdN3=u?tW<~Y2i^VJHe`obW6^O6<#B%h^FC+`92Za##M|&lK@-HC5*+hobgWTqIWQRCE*Qh@!mvM=AdGU(6_CiI4)VZYUODzLwHVjV5DO4 z?*MJ~=Mq6iY5s9LD%*lW&m;1I+v4afV{~S|A*PBt)zajzOwNDYg}>xcfYc9be(PF2 zm+JC=f2E5XwzjK#SoE{Bf)Z>X5Uzx$Ly4&;%4D((Le z5!<^V2GZ@~&mab3_^CGx4R9)`bi0Q&=mrDKoH_%gInK6oM^0mMRv(fNsE@-jN95qN zm^2=kf7BrZQ#K6QEjGlj$a3#ba9HOHC%`fu(g8%SR-AMwmj z1{FHo`wvVRTBmvg&CcMj53o}zFq<@*6-dK}*roQLEi@}>dYP#RbJ#)6rm2EsX|Aa*;QPVQ5k#6uPj*e}xPN(F~koap( z3VyT*Pdc|L<7V!)eY@!MceJh2O9EWU*>0uYXX3F%T1vr31-qCyTN0?GW^|03$TNKfO&R+LM(juuzsKMz<(kP6CgJ&-dwRq1bKG)kBvSBVGL zui>#mcl0xdS;gogmu7xOQYRJe)yBE>&z1N!>0)=jlp457?qC(pkC-@P!eR@wjD*V; z93lsg>_Qgq6h`Jh8!HiQDWxVzq>8N`j6rfNi9Qmu1FhFBbME3>wj*9h^<3G>7L3>y z0kk&vJ4OHj0-RXEiF7j<1pGMDflM=0+v`$_e7Rj1eD85*8rP17vAKbe$=;F|(?P{t zyCXbxSe?+Gw^hKIi;iaW^Vs*7--PCq3o9J!Bno+OM2}mHMPCGilVicXFe{425hc=Z z$E0%@$|Be9sO7}7g~W_Jjkc4!-lN0ww1rK1)^w`n6$aqEfCu29e{v!P%W*5Ika%oh za!sCq%se}!l-}HM97LvOsrT_C$PWtAW&i!_wU$!-GZ6ivd1)vUNTJW|lfmM9afF4j zk<#3~JaEiR`AHXI8ILT~NuvgMmw==F!_L>H21}I}G?lup(-n<$F=1i7cfZ(K7iTzG7r3GfvR_jgdBVN3cAeOg6`&rJbpz5*o3t&1b5(|I ztPSM=)(1DjI0#ri!7x3QG8su64SVT*&H~ySOdc6>Ib0V-vkUGoUAx*m%PjOT+~&*6 z;S--IztZ*rG4c++^4}6zx8-QZUd@|%BWKsnhPp#5tHoO{)5fvwYMYnzD!62NS9H5( zYo%`s-m8*r$N~osPR54CZc~1IPW=EirN?4zh9uFLrym_DU02dZy)EiZA7wt$LIov85pn~NLCR`5X(o@|+MEEs{%O*myupBz) zPk^)?Z}-lUDz!27B4F#_#6K zblo4>weRchErbt^7ZuqBhvg4GGJI3`ooY8w&cAKzsRtO^95r0V`q3=uI{c&+**=*u zvXp@l!G}# z2pccz>1GJ=t5qG|RUVQOx*$<<2Ux+xA-SuZJ++mpe=MRuBscmI6@O)Y*GA(un%$IZ zN=lrg2Rej$x%rx)w1uMpBL7Lh_Eb)|%UHX)IcT{W)9gN>$%|n0W3Mosnnh?jGS%qU zkgWy-fsC!h2yUY>K2#j)L@`wNaA}I!{WMkZwdH3QI}*D&hQ>GaH*^+N?M0T)q({C+ z>(Q?!^TW0;h7>QAKX_9GC66XVI(ZX@QvP@>uF0hG^l;f3lV!uV6lJjI z?pcD&fmY_Y0CBcM6*AhD$VD!Gl4}*>x9@u}Y8wTrX6qj6_er%ldeUYvOKa{c_`4(-bHm-mhIL7prB+?s}@Ns02V-obETeWMrmE#`bZnr6pb zO^IVLQeSU=f7nS4jm`Q&LjnYsd2zf)OC(Cv#UD!i*d17VGB2!MS*2PPBp@>C>BS7c zDH*Jn3q9S zc>3K$3rm?GBCI<3ZBUm@Lbm{1M6>OPfNyHurV$})72fTy#3slC2h+l+lv=p7l zzrNv8S!q(jwr;`;(>6peTQC17*@N3K-ASX1>2JHQAa`_XjF(zNr(BKLGGd65F}l;q zafoKbp_Uq|hye#YdL)la+KBm%cbyub@oF9T&*#9I;*A?vy?gQ- z(riAhr2;!s3eoAeOVUmg3g{dDkNpeeTcGD=%=qnYM%gQW|mEhL*877y(a{#qg zH?dro;(2=SKX@iNlYDrsgR?9V3z*H|u$fP%-tEbFom9#q3)XuT_kidzzx-|sN<*}+_ zSjTM|U;X{P=Kv|MM881W+p>njkVHinT1eA-!-OJ)lb3_fD%;d+2DWhf$Q0A%S^cF7 ze23_Fpk(5_R`k0-cPhY8;6VIW9EDph;3w_qWYG3WY8@(A0dCVb@ojG!I;g7Hre3WT z^?aS5d^EcT!Axl9HV_2%1p9VYEFbbSOq2ODPaH5i!e{Ock93z*kt63=$iF zkA7TVZ9qBXvj} zmDP_45%bQQ+pIl)=!qRFD@VDB2kEGR%L4^DT#89jTwIT!$yV|Ph8UU{q-N3!@}4xM zK?MfOBx~!Q1Q<8W>eAYDD3PQ}^}WB)TIv{-bem%P!Ws$Rl z3R0A!gPPx=m*@P^)(>`thYotj$0m9p|)cddkO$6|&T$C@E znHU$xtPCV8_;IN|shF9WPnsCnoFtiPzFd;YHz3EYNiH^X+(vYk(z~9rHY&Td`5L@9 zC@tyw88n~${$$oOA7w1V9+~PY+wWf8M1aBJ@vvV`IbgG9Lek!EJ8>Vk%6m;CW)QCF z_8k0`Rti#lZ*2+pS1ABY-pP5Z|1(|L9%@M+NgJ5-Vs!E&Wzp_HDkT)I3Ne^FM<4PVw5A>i! zBS`t`oe0$|Q_V*_6--_{pPx1G5i|CFH{>14VP+l^s>d##5E-feI+Ur>mG4uog2?0p z6Q4%H>1H99&q;OsQ(1=IM)X)c({F6lWQ!9j9sW4t>#o82Z&jxEm9r}+1*%JR>hz5o zP>xS5C0EbnO%0Q3-SsFf7VgV3;5fW52Sdtw#1Wg3o|n$k3n4&qwr5OItR~Y{BEqM4 z63SGha~A9c$c=L?U43a}K|3!cLVrQ+UtAszY==Qko5>rWv>UqQB_Q4Tlr2ETOw~h| z;eH;hvwK&5R^jD6)IWI;s&QlXW_DI!V#+nA=z$GEA%M!z=lF=YCK;-7Wp;e-D7m$K zoH!1^V>(I2tQp@$kL;|MF9=s{Fln5n@?e);@kVWD&yZN3?nucGzjIP-Vxul1tlX72 zD;8i~v|3DycfYkF8X5SOdl<{Bm{VcK`d(*>R8rAqB@Rz&E`=pBwuyUTkeF{2XdzR2D`Nn*nPNP>P7#0UZ^0<{|CS*8c6Yc7$Mxp z9t=;+@*&K4oDSTS%M-NS2IKYc0*kX8gghVPM$boW&o`;N-?4(7->2*lYE-4ws}&@6Nx^ZMU0zC%M+YFchoNrWnVVEI7>d z9?%n)zb94V++Wv!Q5hAz{!x=wCLs48kxxf2Hug^IfYIlPYdPFZLs^srTx7!sLM?c_ z4k{4N2@igPhn+u58KmCO&cWJ+`1OT3eKp_~j4=5N;!t0RlCD^b{z9F z>ohGR!wjh{D*gH*jG!(xBVGf&Wju9-Dqa9isHq^-_)anMgyEj`YWU^V@Ttl~ufxOFkW<{8j%p@@*48O?hwF7eakC}b^Y)Glvd~@#M znzw>c@N2+~*?LTUYG8`*kTkwW`^VW6WoeplwSZsU_F$h-iu8QJ2qJ4Mr73qt+Xo)X z5^rZGeoS4g8;;SZ!=qhL@0TB^cd<-qD=iNAgHpQ)cK{W2V<5?ZCNcCX#kSeb{f6o< zQLDfGxBCW1KUpLz|9in#O9YrZjG~R z3K{BiQNI7{qqGyQ>yKdj-)e@9T~6@?=zij(*?(U7i@XLzkY3c?Ea&CZNK8hVgikLY zZ2G8M3}vAvsgwHlAUGeDPhY3qhMqavKL-gK9^Ip0gIoU;QY^alZ!Rt1e^gv@L~J}W zH*8v#AV--U_gbL=5S?NL`dV{3Zvp~uiEN{t>L(HNljNw}{2c`U3r(p*f1ajI>*P$3 zE&-7NCdq3e*}F2*o%7W=vMTLOh+@K+>VG=8em(A8Vk!)c>G@_)v8TQOKE|%w-73iJ2DerDo8z7=OA5?s8o3V_`70SdnAX2IUxeh~nPO8#fL#xKrH*7;{g zKFaJcVA>8+p?5Y+QUx*bIpNu!tr0AvYdTPH6mmMAzhdi4)sLE;e%tg4HE9;M+58G5 zD8$}M8);l3mAz*6=5@+PJCe3+wKzYcl5&T+vA-uFft{q`@l(yD;5LM>oMG)`S;{3N zSs|)zn8>K|&+=Ws@?SEI!lEVif5!%1yVW41L! z@O4B*evs2yJ2#OX(vLvY3-k;)0fNzzLU@B$b5J8ARADjskT1)c+E8He!S-5`=#FjT z6N`RBpz;VvPl41ds97Gpk{IrJ1i#+n_z@tA$WErR_bWYCI<_zwX^>UkiWj}LlBT0K z>?|WbY|QDv)mK(=qQ|*0=QDat9r`iaKCxF}vVY9weYMW5<&LMHMRE!_G^PMz@qw5g z%@E0l)VCX>XiD?iJvj92)R0nGOiut)&5GsAWiNL=K5TikH)|~|%0a~BKgyenvah_o z8ZA)4B16&St4cd-GXQ=M$d*+XECTMty@z?A50=liq{6paqd{cn$f!-tldT5&vT*yG zEk6A*7O$RmS5d2?XBtg@(_@3(M6rjhoXr#liQN)g*B&Uz+j+omy4>xzA0WAuFdShq zUOhqVE*bvmk0<6r(GE>~$ai~r_v=7MbKF<`8y5I5vJ|_(eKzbDBw`3tIv-&%(|Cd~ z3>kvmjUb9R5zT)zMvPf|j&V8Fj28U^;N$|eN~9m)p8# zT09LEK10jxXNo2tvbN%J<}re~dKwG|iB?MLmy$==cQdC;i_{6Nv~A52cvyCAM7pk{ zNpsWaas>0+X!3Mkz3qY+g{>o1oUh9JCC+@Jh%=v%{lW{E8|Nn9RHJOkWzOyBPTpn+ zz#A)T3c=DVe4&_#(EO^QS;6L!2R_ERKcnn4S^NpEXi<>Cc?u<& z$UbXc*-G>r>see0Z0ATuUpCAuBOmxgQ<(qT_~VO#v5$Y6I{0*ZDcppZe)R_v=eV(b za&L8OgKj>Fr|h_pds>A()J}8o`T}q!CS%H49;>MElqK5f+e*>_CUYx)#XanoEv4El z`*LR*xM!Kg*C32m(jFa1x7ysG5k$8O^ku4o8RDnrLndo~-69Xa=c!3(QVgzp_26xH z68Uv|e={0adwIT7zs6l0+e4~mW45(MfbG}vGNs~%eSwVj__ZdLHGJA0T0BlM_~_2b z#2(03IRKCRm|hX?lK`=-xW<@#h}qHS1gsc=K~ z*$TgZPe9-$9oL7sKzEO(PS3)7om$;;3n>hx_T?Lf%rz-6geMdw)iu~5%H|noTVp;6 zQVrn=tA@pud{EaN&7{MiS=IizXEkbf&$jTxzo<vHKl&eE1-R9=NA(8x*;M#NG@Cp7d#jbEwj{L zyv*9(FhmhzAqV2*e7!#32nu(_D==>Yb?z%8$`FF~v@+4|gt@~E34%;LJT`3qh-%O9 zYbM4|TzP6|j|glLjbC!GS-EQOcw$2nd`WKA?JK4)ZWrFtqc>8?&kcYzPN~?5 zf{f^8PP%@AF65MRn?Y8S@u4WT=+$l&fE&+;_2qtHD>t4c>h3+NR*)} z5!$y({gw|aL6k4c^Tx;3x&Qs52Ef|5VCgDOedE(>tC<~V{VTbjq66 ze%L|(3vv*%0u@dUeISz5_sOc`U##v&?h4|Jg#Lqq|7q>IpxhG1lB_}7a(wlkfbtjr zUwr8os78~T1a04#;0Ri!aei!ViBm1FGt5OFe<=h^+k%giN4+;YRhykR)=2&J85+Yh7aCpZ8$5AUz zrDsWEjX))%>sxvg@P{D22)nW|J{*xEby7E!gBh{JWgoZ4is+`<+#K%E?TyEkFs_3W z-Y_%A+F@d5B~M2(Jx7P*kaO6TxQo|UC+drI8hT?PG;tye$j`b5A%RR32POtq|_fU}F{2YR@Q$@!3Haqdoek*2HpdeAD!y@dS@9s~x_QVviN)l%l8kPUo`!#9&HS^2!={ z@+d{y4V=^Z_K^a>h<)}LYh98U2zBxf@N8alT{#iFA@Zu^S|KNY>?*xh~4o`LZV- zvsV_tE8sVMFK092o7(_x15qcdWKKI8LNpf$dveE`O?wur8$Gv97t>TZ`iXKvMT>@mG!#dBFSv1=M z!XOTI-Ko?pQHpGOfe9#7)Su!^|1|#FwcR_f^KX}iZ}Y>b$$?alPXH!dd9Bi!`ME~} zvy#Pw*_?0fNinvu8saq>F}ag>F>w!2vc2kBjs- zPFvqn@fF)&CvNNl4ZpuF3pWoM((^TlDj3jqxMR2citcN9jKY+Jx@Ki?LtE%U-5e3#iezM9 z(IN2)jO_fz{>Z42p+BbI)PB^`pSMMmTrIqZGur}C0yT~@jF+07Z&vzdzFiZz z_=V<{CojR!V|bRknD+BYkZz?-F_(ppl@?I+&3eQz)TZM>TN8uH_zGTN>%tdoI`a02 zd~zn>{b9l%cz^L;ZMOm+Y|f15+7D{r?zZ8QK5(xyZU0;VKjZM7Z*C3GVVji`6mOl) zXZMivh#wZCHM?#AC(XO*p)j4e|a6 zkYOnHs?}uJ(-cE~7!N-m^|9EP?*;u&rdEEbW8_N-C4hz+HL7zst}B+40$Y{vpSlr1 z1N|Fxg1tsC^;+S0$LWXT{bc?*BZ1}8c^`kZgus!;{IEf!EME#&@+jH@XKFT}n*~q2 zU-h^f3tbt3 zbvf@Hf6Vtip}X&6Jag?{+g@@S);a9)#LmlaK5Jj75=&2poBHTjacsm^BvgytXL>`u z&_ioJnlYs@5~8F06JWi}SD}qy3d2S2$&qRp)gT>-ag7WvH;ly5ar)FN-piCcqw6zP zZJL{Nd1|i4Q$I09^0f(JoW~@5r(Vio&G(-61{~Ulr;h!{8+SxAVr@Y|D0_VB0 zODq&k`zl0%HdP&V8cNZErS%sEyIW9Q&;n%C;w=J@&%X7Xemw|Nq9mBQ+(f7%bT*Nf zGnFb^)nGyAGspowu~@#RG%X~q%{q0@bG|(fa3%vSOODo^j)u*x)4sj77YCKPm<^wIH z#xU3`?;kaXxbWAHYTRfh7|I7)fS(QtQEsoFTS|$dODMQw?R`IpV~SKvLk&YJUDAkd z6?4P#0oUDmZ>nxvTd@R8)ZOLS9Cr$mE6a)PL7|&a9~aWRA#4CI8lKJQ(jB?j@O<33 zl3C_BS@Y!#p`dm^7ADdJ#)?slCCvZ)tiBfgGEJ*d9GAuKYSkmAK+VfS4!*-%{;z6} z!~9Z?@{U<_O2gcyz>`)kDpjVICm*it=;&i28dLsbV!EZSy>2+vnZB43< zm6i13ZGQOljneJiEZo#GDWc6^`B=w3a*Nza=jtwN^DfteKjv)VlmTn*QfldVd7DuH zVRl;KkI}xJ!10lwD`Z;pD7nx}@aDMsbE(8Pw9&d@+4^JFKx+e5!~R+zKYis6+B-Si z#$r>P1QWT;N~L)hQ1)g=Nu$G?#46DWrGlOuwk-Wfu;e=kNBT%76YMKnyTru_R~gf1 z7D`*F^WT06Gq8Vca>5N2-y1NW84t~KyE_vcP)^pV>EyK#c=Nf5S&<3Wi!q#cWX|g= zXGE)fX0bN}#}?S}K@pB5_gZQefzrQB)`k#!l2yKYNwJ0|3(wOC${wxDF#|4*GTv!- zgA(U(Z8OjP=z6`E4!_wfyw2cTKeYDq#{`E3|B|yFQEB6DhR{C&$zEHFxKnfhJRyn`>YP-j__6z1B%h*vZltS%m9`lfQ9ry*?W+L2H*DBf3uT{ zBFR`ioP9-_zFT=!&SuN8QybgoZAV|PD*}P?qYNq?u zXML>?os{;8T^Bv|d1AW?8~N#$t+!LEwMFKQ&|#PYU2Z=}bTa*0f8I7b5dVPMOPnbe z_Li66idcIuHhDDiG>PYp&qz|e6bO|@kK%?oMg?A}632w`+_Hv`G+7gh7MUhm3BYZu8qZ}06ckpKrLkx9J2}{RO7n{Ejk0=$b5G0C^|>9` zFWQd_O{J##psV}$HIoE&IhkEKkJCe0Y9nGmRD5FdjX?`{D5d&@b%&_d-q^u;937&r zTx=m}DWQG!;+_df))BSbBY^fivb;eWK^~VW;Y;;%j!oMcO}A@*FQL9+uj*bycTlB> zC4Fpl{~t5L2kX5rlVSmq+AbE{UM#wrn|?f&Sge-Zl7&Y~kXRiQU&ItN2xsjX7TTPt zs*B+H5~9N4JmZx-Qgc>*Nl}9ZxI5^l0d;ikxamchnV>1HP@BXT{U9Z3YS*Vk5k@K6 zE~ON2T~PvypL+}E?%1SWirNlRfBkMZ?8?Kg*jnG6-e3r=jMjeCVJ*|_4Uz-%rQ~oAJ+M?F{mQTVis{{?dB0@Qt0yL(q!4}lsZti`=R0) z-a-Z5^OQ#2lccz}La_AkIKdKpFWK_tmj2E^z^iWGA2Y=d>jms^M%sjb7WXgl!aQ!W z?fzID`WV+tt;fDMb=#D68a1Zat15W*6i^19{5zkj^N<((N1R zsAj}9HPFYieev4FIo7tzvvKV%nuQd4(cAhv6Y$w4YU0IK_yN(AcsGSRYK|bw#;>WD z4zJ~F@VHNY2{)B;RRO05(Y%Q(iv+}`n_ZW|ahPQxB|GT>Trj7{UtL=a*hUL`%~LG3vjaZg;Em#t2kR6s2)nIL#^k#% z|M*smvr8O7a&392n`5tg8r$}UP|?>a%{JHHsylP?H_L{ullY55)U{=SNR@vo1oIHJ zTMUbNr)GJnKLd^^S5*YWSlge?hN)x3GXLy{kyUy(X}Gu6)|>}q1;VE^DCj~-t4-@6 z2|Rg0*caD|Rovh4hv>EHQ!O16hbxqaoI%{vN;TPXFcOux->f-FG_=gBTN^Xp56u;_C|$~~kL zKeqaf4ij@oCxB3DE&cMS$dPV}!{ww#5 zbG~r}?h2_LmBx--W>%%K+No=QN7WW1iGeR!ff&RyO;RRe#}*qWyz`h@f$;wK-?0m8 zlqUmlNvTd^22@h}W+Y3ri#-jFOoR@|)pT@#pJh5iA#t@jpxlx=R6FNMqmb#el`)*n4s+4FBizimKyx(|9Ca?6mQ^h`R!f-49Fhh6 z6{ydoyCi_zdx3SZ1X(}6;m1H;eZ7P*YsFZz4qe1p2`!GqQ?8!1?Pe_Ss>;`2U z%UB}&Ffp0QHkOewX8T>-_wzozzt8*E>kpUtjPtyX^Ej8|Jhty)e1*K`AEnSuZl)hX z@)X8v27(k1U)Ct z%YF1tl6wKfmjFb%r1aw`8OW%du#K_T4^=7xr-EGtQAryvmf`?)w{Z8Q!Q$p7^|=X_ zYd9ab6Am>h7~ra-kf`EFR{ea|={(8E_UtRovi&=GRUvsu2S9oJQc*UD+lZCrd##-) z`>wa*Skd=F>R^Qv=onp=dT3QQ(WK2(x6;L(h)OyIBAf1&hWr>k?gE7O3Td z7Nse8*(Xky6z8ZV8Ukwnul#2`D}c+V!iJdISLTz`7`&b=4om@t8BG{TyC4M|X*qb;%Ya+` zfAH?3`E1%e@l|X}@=tdH7!IPne|&1u<8#=rtqpyzG?gmAe8* zQjUxN<|jrUr-V*Xb3_#nCi&U3!^ZUL6gx+neg|kw9vNj!+i~a5R$eSpM_1!RJ3WZ< zWleeQ6GwqNmmYxDAyFWdQ*P+l^nw#o{QK{W*P6h=N}FXfmbI2c2@IaWt!aKljlZO` z_A~7mu^a-lUn#7AEknRI+hWlc5owrt06^t&3wC#NhxM-w^be5oW()HRd(L;ikIkx3 zGAk>gG>h0IL0aq1)!K(!ponEmd-0_#munz-u-@Iq`}*;(B8I*Q3F-I`Zf?=oj+M8n z?g-m@h^IYpD-52GOdB~OqL=2~&?F*1l4JO*a<#gVdXPGrGAmA75im#YID6~&|8uey zX0^U=f&We)-|bGU;~#~KOG#zsGHP=z`b-XSM^`PB#7vAHP#k=Qu z%-Ic)-Q-Hx5!Be_UfdI(U+NWD=$&Q8HA}25#y|4mA`p49WDkC`O}|xrSdkngb8a4- zrV5#B=S@87MR!=Py~`edX6WI#oON~_$&%GZxMA&Hh4f6xbH*pOoS>mTs9L3;FAT?4#UDB~HaLeNKn zs)BaIHe%RBAZE&zdmC@e>o#zA*pq3NF~}eG%V<8BueE_vxA3B3oSvVV;4j@>kosW( z<03Y%S#{tFq3sZ;UMbq%^&Owym;!|z8&wa3^i0>qml`=jCZmU=x>e%+kv1!6FavD^ z)l|?M7NCs{$>0!fj6z)LpjA%2ea0TeFeA6uyXoTz6W6aX`zG#d1?sHK`h0S&y-RJ6_62kTG~kv+gmR2AtVWuve4M)d%7nSzCg3 z^wtf)A*WSN7%o2@x(XOyXZIan+m2i3IE-}4J?pk> z=Nl~S?eu+qAboXfX01i}HhA+#WLuN9lz07-Cbh&o?49-q7WX7;Eo!<%U58$nF^=on z90=-K5muAh-_F0rL;QNQYu%wkKRdtGmCkityS+)tCon6rTMyvS7eC=)?({%_$L&W* zI#K5nrq>RZ>%6)hUQ6fKKu>>SEI&M{QfM>|Qb!-hOx>u%mBxjR7?phH1!B9+SJyRy zx@`YS5p1W<%y)qe95r`-8h(q*v4050Cl&RuWOh6404aWGO0R$ zW4ZZl9juJ$?9i;4Mz`T1jwcMVKi}03yG~F7>ird)9Yl>uUf@2T%V4?AAA~s{y)=7O zY^3&w-MY$T9~0O2J^C2A0)9A+!G|$Xrp_^f4>%i^csz1I_tEhSt!s;7IlKdr=7_C9 zQhNAY`aHzb%-I{kCLno+lxdvM zE?k{9=NBn-LF!4!tvA0{yhr;>`$4To*<;^&G4JUj|b#Bl%ae#nlM|>ntC??WAcS z*zz#P!urAGo#W^67fhE`%q{@gElqaj9`*19=BYdW(}9th)`9p_{*x*SWR!E3_15ji z5|~j7?0m@MkyxL)xuPv$Jgm>44bcty$(@Jy6Q;zpEr%*S1Ed2zM4>Tn7sU$Upv~TM z#>7eH*BbvkUZXCx6B zoWRI_;(@kVK}X0L9r~=JYaDV9YITvfQ9T7aUOxAXh?|6kJxAIEvNOZiHL3B#XPC9Q zb!dE~t&#ko*2y@lRh!TYqht+;E52f(bXkL#krPZ3f%QRV#ieOv?Ens^#N66@S5B9% zk>mF}Rd&`^!AipcOAA3xN}e(K&@}L0{y=r&7aZ3O-o7+M%Aq6Ic;Da2xaoX%Y4w{e zJ7q)wc5cvQDW<}2@|ih&MqqxgL7LP|IFl%4)u)p*{7u$Ay3=>bUMXyV)NYz;q9@co zn9!mQ99U*f_Tegk$t|`=E@Ac6Mg+IynJq0^XK6R@qbLu*pRUj2Wc!{?t44dP-McHJ z*sb*lA~CZD3p@0>aP9hg*-L$47FheO{KaV!#>D-p*{7! zd)xy5TMe2`PYASf%2|KR(dgR;3S~2cVbmV2Od!|QO1dSg*Y~BwNkPfbW{WYHF_a|< zpk|Is?A)NvuxFInuY-YW;QO5P;|FKD;#aE%iyfk?rmF-*MIQ6b&drqsuYWV{VbKUa zh8%j|NO{dcptjoqi|SC|{Py7AKXv`f-f@G5SL7|!IplBzGdU4{hmf7(tkVI%9}IkW zayGYhW;Be_JgfMvD>0PYDLPKIQwgzzR}vX7q2fA~X(50X!J}xZG@f282cVgZ_RVC4 z##7B37tE+l@SjsqC($rnJ>^@BC1v$U6CABv3z=MOkQnZt?2C% zU{Yc{iT}kQaM&jj`QbmfK2U=yg~@kaq{26+hv{B>XDN|s*fOrj{2{?F88uG)4Xf>9 z#+)9~Azv>=_!(fSJPv+nt|L)fPP*n2vVSc-x<886a5O^a@32GO5i29(?f;=4F%EsarebY8~})a3MDg4q3Aby zijC(e*HyviMR~}L4I!1AdQ*&&6Qxt>12~kq0n=qtQiVmJe(ghYR*Y|%L({RNpB^o+ zdeS4nehZQQ6Hk1?$P%P)vR!_wA6c|P{&+-65&u6%-1dG$+J#Qwk}!q$oAV&i=i_I& zo7PL|>nTq1x_n!@dciej*WatA#g{0wjGxw0Z^}nj(raWJrU-*wLE8FnuPa{i$^~?( zzdeuo)Xt{O%)Orw);San19A{gO~g+qM6=%Ihao?C`vUISGk_|kvaD*K`J=Kc|FUuS z$S#=})l;UZG-Dp5RKiU&cmTaqRqv;$8t0HrxbQ^2*Ut{QTo3sryl@c?)`jUW(xFS6 znPF7LR^;LoeDC}JcpffnoUq?mSe9GZf-j4)YHoOAQHKLtMXfq*cIx#I#o)UG(rOH< z_x2K4ZuB-=VaoO40U%oX)Sr=@rmA`%UOtVA&k*rjPUzm@sL8ESt5^j3PDWo>e>B3x zPF|%@5r9nnirzARv;Z5bT$gP=>c1TkX#Fwy%jF?2%SvsR>T}?kJ2Ron4O%?xCL7ut zuXN;tlxfXx5YutZud5i(=!AH=k#w(_&9Y|(DU2~=ZNm#@%LvTqd&27RiOXt*$UTAj zzMWa+HH>d)uLT>F%|6b^(Y43<4b)z}_x%;V)OshI!O1x-RHZ8T_8^bEIM} z1$bCpZk4S)%u<0UdA<$QzCu$VD8@w;!%+v`xUlu44GEa6#=WC1zktz5 z+l4`Zubu*e{Y8OYqncht$jk5=zT^PFB-Sy;)rKoQAhW>r8d&?Bey?{eaUP z$e6ttDR$z*zA^9Ps4?`OKl|-b_|`Fb$Y2YZh>VrmB9ka72JLq4Zcf-0X~ZD?apyUSBf|MGB&NHf3E8~tmB8}S9s zE&zMttb9Nud2)}dp>Jg^tzh{Eu_(#wT_tgDx^Zj*tX_P66R5FJt!vqrU)tGd=$Pt{ ze1pM4E2{y|btSC!KzD&#>7q z25IAJ`&B9&D=`)PSi6LHtZ8d6HT!qb=-Q-@%5r@BgcgDGzN@y6{hB`YtM;Wh2-yEQ zDu@+hmQQPoXN6B5^c^mO6hvy@n1=;_F?^%+G{!h(})PS{VGcKGy2T<{hl zQ+bND3&w6hOz8jKGy>!Fk7{y-isB+M$!|IUc)Wp9qT zU+(o}2 zRK_?yUR+yR9(%vE|QfE+`NPHmI>4xmVV>aKR0MAPQg(zfXQuKEm)4OBl?6)&`P<{DPpkaxW?o^we)Z-U_j^Id=N|@w!}|44wFxwkp1b!4QglCh_!qUv1EHkCoxlBKjm9jy+osrR#HJvX@gJZLRI(E&-c)_X+~+P z1K}FZq20bWrHq~PR{pN&LQ^95cfHwgm%6dH5SNAtEsrC24dI3EGf9%=15)EvZ)d8u ziaPWJ%j>))l$Mh}nhy^-cDM92@Pk)xhuQZM5*OeztG#s5E>-j+|hi&CVj`FCH z{Tyhp1zhpK>sCE9776Utb~Q3oIV15Ub<_KQof@;Mai#iuowHVIih|B&hVa;leqU}l zlBvs1yMoN) z6_6Svzb3vcdp70dI$z`Mw*wmHuU(|}xa&U&atxvvNE1_|^Glv_tpe$H^9gIM6lvw< zBED-zvgJeR{dl+gNA9QJZ*{F?iFcTI7~5OyR|bLw2Jp%jujDM#?2pXNYAm_3rc&IK z(%-wzLa%}oCkPVE25tJT*M#Ig9PPyZXIZhBtcqV;>!;k{9AoY|rNN68Tk_88^ zPpb_?4(1xF@H^DhraBgTkuav=1Gwn0A3h%!itn9BB&M1IF}iJ^)IGlLZI~ad8r#R0 zBSXvirBsc|)lZmJoG&5YoGvuaIMPbRU4MOj+Rr~efT44!dUDD!Tj6)j{`fm#Km_86 zFw}&Zv%|@b0_qg{Hud6j-sPLszsNtVUm5omOv=(Bk6ZhP#q$6t<3c2By&zDieLWYx zW*iH_1i%S}+zr~&QW)|`tU70*oMZGGE`}E{BCSpWBK!Xcbl{ZpTbYx zjr9^h*}US#Q!_Z-t@~OQ2G^`}mQRl=9LsF%fCltHExjMFI2pWoWoPbXwEnXY`{8QX z2Rch{${8oY>Cc$pm_x#s`AG3rtYQ+jk)v0d+|Zse!Ol?)WP?fh=9n=&8-$oJDKGGP z@y*cNLfOshX&oa^6aCxY6E%Ki6mUZF(kmGs;SU8ja)(DIXGH|fC9wlRzjrxHvd&Fr zhD2UOeusQuiO0Mjs-MqPf%Aau3VW6Z?QhM2C=Gw>|jC)ZJ%NFf@)Z( zVxlhv2+Tm30A7bg19}i^*;TT>n(9w9_Qc2ohzwE4FrozbL;Gqnt(1#)rB}ak;!g<( z6m-}Wvk5-XtUc`)2|cIf3SFgLJ(Gz)`V^~lT{2Kc>oLVT^vSel*h8vNk;K6+MXhsd z8pN$-m;2UGgG#<{%dCOS9tSOJuFz@jIQmbHGj16(jv7JS4PRP94B^)(P2RW;9J z`2EA-^ucniVlcHDM>YrlnEiG_{fy{`o@%4z$dHyLk*5!hBpYXHZEHthhm9wao>yd> z3$L&JqCJg!wm@o4!Ioe$HP4vFI2L=~6Z>wBtRNmKXFa&L6z|(}rW4bxDZij|9{D;9 z#anh*E3mEd7EQ7}qCVV@P`9>GVz-D6Pzp$F-Ex@+Ht0A?pqC)+K-G8!fu2Gzxe$da zCF45Wy^&3uKAgH>W{K=~ZOMZFwU931a0jJv*qe=#3c0;d03w-MGZGg8s-Rehn?)7w z*NRQMCvdhg!cpRwn0IZ2znG=Issizm=U=+#YG+Bt?7v=Z`TWB#_V@QVVm0%wyEX6N z)>(+zhY}WUA)daT&w}4Ir2xqB=4?e+UpW&7o9JVQDJLd9lu;bV-SWwh_-1;;O3ICV z(K^U;RdaDPZzyQrQ))bWItfLeZep6OSIS)!1f@9 zJT1H#^D7!wd2?w7QAcO=6ie0DrzW=71jDO=%db0dgGb#MuQ5vo3?WUn3W?{1W%a#R z19TxWQX$FSzxdDjPiyj>(`)L%{8FqAhHhkrS1>cjG9E6x^@{Nh9+B^{0<(s@ zTG?I@Z%+Miha`%C*|-K{@FB3G;|O-5fHwW_YL}0IlX*V%uYd( zFP5IZLQ};+B-`aw$X%K1pGfCwp~t?w38ykR*fc*rdSxG{43$&yC#{D;=oB^>E#?w~ zoV<^ZJ|mRjq?mhX2Jez7Mz-UBC&ugem@YVQS~Kbi_rhlt2_A-Q3&dnN+u6en zk#~U8fN-X;t2rH6jWkAsen7;GoYBt`i`9#%=5J1)8k)+yI~QjA9jsC=&b2@suxRp; ztohEQ^M?0LugZ3F%8He5NP&2>oQHao*?(?CVF;A4TAz-A1hY%6QOG@HsT!AXaw8VK zi7yaAa9qA=UKBEO^a*FkvZI4HU_R*f-vnXUvRxdVInl$*&wQoI-#C%MAh_egHt=EK z2RY11BkF2k!vLOThhIPP*&)UX#e1aOIHfYeDF<6ulHI*$-UGU;XYtXWRKy_1q@U8W z_L!)n7mim+*DaCRKGaWAh(da+dFgJ)I|%k>y|IGHY zBftn`vhC)74b&IhrUpnq2b9p5-P=!3EdZ}z+k4Nm{ro)n--a9H<_}U@QkYe11LJ+t z7HA{r#%SrrM$dE!3l3;5ldd9^Href_B4*Xet&%SMVkM{{KYeD<>@yvp_DLjMOw$!w z$+~?g0CQy45lPf_Y~l97^kV40ITeCt`=l4{YV~+pYVUaaeb;b{Nex%oQ};b^o~y^t z3tNZ6t%UvlVdre?diy8*3es;el>a;W+e%X z*#>$Kx^!|02VGcphY0xSCF)AzJiH#6a4jP3wwZ{_(FZMDcX2ye>5J@LIzOzT4y|L{6%r#|1`KL>)$6FJe+Sqn^pS0`O>$uXOOQV@V zkKyUTZzX;x4{v&HR+viV!j85)bJ0GIHTe+%oG3B1%g;nwEILapUru`|^E zk=O(bHn7B8QqDj(CKM69523(Q;RSM1pE)fH zbmLO%A_A!7eD{BGae!6)`RZ8^gQvUuhqPiZj_sNjpJC&I34>Mz^<;pVz%F{KkP2Y@ zY*-N2%f$sQbZiW?;MmUHM{;m=R-S8)oSk)~Kb-;q<&Jq*O?P^d2>Pr!l1Uhx5z zQRW*pwB2tlbg~oZ^fo`n#MRNPZE#x8BW<`c)<~R_C;f7_mQCZ>ZcuDshM)8{>MpQF z_5lAjy)&yG$n5o4J?1{!7)L1DiILQo!bs^K z$NUJi`@!EdxV!IHc-tszMF3X{KP&H~YQ}2tOhfWHX|(Ub1H^m;Rrrns(L~I|z?BC^ z;&&5A-rWddl55`X^F5uu%{ZXj4hmNynJKx`>v6a=QyPA6v5DwsejidUOjAP-^w(L% zR~7@$*XM$kMT(9^F?o+26<)qbe6DylqPXg>)Mm!Oe@G0F3eqRjez1P*n%*ESYIG4@@)Jum5i zFI4ls+v+bXP{o>@m)nEs*6dIIq@BR79sUsE+gx{YyEmC=@D=pnvL?JiTmAD*5bsrx zd~WZ9=wk^spo+~FhocA0^wcXiE6?6;|L6GJ=5YP~1Mgd+beg!Ag;3T(7ikc4RB{yd zdt527Jd3O16n^f`&iAKSfzT9xh^yPAvbdfR;7-)Fh&Ar)9o2>HNH3rp7ej5J3vT{1 zrV9G6)jw0xW3!Vbn75C;l+9XLg1Ak;vnU!Cv2*p_kr=?d;<(AQEKV$Pbmh5cLh_Oq zG&}#&WmLX@k*0bDs>HwD618){Z&OJ9LN_3DE(nN;+PL1+kXgW$t``lUt4^9<1Brnu zN{TDiu6_Vj_2PHLD$roeE6X`mn?Q!=fj$njMw>$*mjM&@+?_j}`|`T`yNWB$Tz7k5 z1X=JE1T2_4v#G}ahMcljQ~lFGhprP6ur|I^d1)dJs`yk~(feH&BH!~tz2-klh!n97 z@8#BY5=L3Ox@4EfiP#+4`u?^#Mol2y&+S2WW?GS@sGYla;n!1tcJA0uH*&w1oy=#d zkR++TUYyM%(1Cjlb(6^dlu8Y~To2@ZsaUf4NLQR@jl;K#xhYhqhY! z!O`qn)V#Y%6>;LzQ7un&ntA>i^@V*%EtJ;Z`H9~#MUaY$>qgDHuU7o`J7tvCBexti zmxt~`&+`RQ|F-FP*pImv7k^FuuYU-BdT5|+*hF6a-%m;O&zF~$Sezl%`W&TXfBav- O&(%w27t1c(eeyq89jxg9 literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/dbvis-1-driver-manager.png b/docs/reference/images/sql/client-apps/dbvis-1-driver-manager.png new file mode 100644 index 0000000000000000000000000000000000000000..b0ff89cc9d75ae34907eab420427dcf109b39716 GIT binary patch literal 60954 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

wwHbO_Lz%yXpnC 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 literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/dbvis-4-conn-props.png b/docs/reference/images/sql/client-apps/dbvis-4-conn-props.png new file mode 100644 index 0000000000000000000000000000000000000000..2027949c401a76ec0e5ec1153f2fbae1a780e63a GIT binary patch literal 57094 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

ILJ13iO$}FTy9sF&V?WLOinG(2`wh|aLFq~ z33+>;&V^(bq1=8?yyQOgSa)t{whKkik2Dq!pPfYAtNXyK5p7be-Uh`t~A zq)OueI(qcRUNa+}q>kQUGT-K`AvK?{A0eh1GI^-#YY_IQzW$#uGie0w0+Fsw8vCCLY_i=$p zHO{4VIUY9jzmh`q%Q}kU4^hXm5#tP5LUmn80i`NmyaFrei!7*I9w|R}N=k9(qk#w@ zQO|e}9tuWogx_840|YeM(d=w&Utx!)$GAY$3yirVY%cTQ>h!-G3h6m;EL zq>Bz;v*BM1*sW#v@aJvWUtA02Z5nRbC5<%tht4jpO`i+P34XInq2n5@1CMfv<;^rZ zBEny3Kxyxfh^D$`(lg>MJvp^FglT?5aj{HE>Awe9&X)y}SMWs; z4jpS-R;|iPQCc0ttV)4fdvio>s8Ijb<_R~(*Ed`&@9#G|uHs|#IdoKgoH$}TUZ?g0 z21xt0vWz^Q!Zw#b1YE7plyCwj^JdNAm!{d^~H< zZNF}ijM-!{=jo$!q<26_Rqww;EI**<*p=i^$*Q>q!|we`m2kCZrJ?RpS3GK_WCJo( zLIQ5DamMix>LX+^YceUGq^y~}mo6d8`XOik0IkqRS^xp_0LB_+?(fejQFVw}yTmQl zH~|}r3ESOjSqND)_ZehoSLNt2R^2l9Ne#8~$(LlT;a=TR#}k9Av3^{;Eb0bv<@>S2 zTmoEEXjTDUEZ1$Td-wR*fY`4|T;6QSrJtD7rkWaK#ZDIsnj9^MYLw*I)h%Qd!{@`L zh{fjS+&yQUG9FCGaOF$g-vlkTe}_|Que7})ehsX1xA{%|)NvH~Pb0;|dC9m6=gi40 z#Fa*;0E3yR8Jlv)K2zC>eWrFX?#>mh8ZZ4y%`5p*7)zo1vf>4mC{A&~*SCSK`l~1R zT#r>Xj_JKbydiJl(_rEXcW37oFO5u5tvNs~R2lUHiF9TFiVBAM&925KMJ7chMJL7R zUlF*{E;SC#2x*Qqyq{}|i}{L)>G!pmxJ#BddX6YB>emU`6 zo@68HWygR^>C5?&Jcv*%e`d6ka3QdUiO{0LV`76-K5Ro&`>D>VHO)$OqB*$`sl?R& z5Z@Z`z1)pqW+&iP^!sA3y-hVqgp`@~JZ~U2eoH(beoMoys9e=wM9a~q8TfwfwJY3} zU=;%n9i=Uk^Raoq8QwDpE=BbFG`S1sEpqx9T-7PbD@yLrcNZ=w$~XzwVkLcR2B*cd z`!_bPy~fDNu=U?6k5=0h(05VNxf%;@MkN>6zU~qN)=Kzkr}pDiho)o0wMNbJO1>O` z55Zr|HejfUzhfqnU-W8z4v6d@ufE(jTk?Q>cO%A8IJ@>5;QmhD*y;@wr6vV(aEs*O z!5UkL0YhyAxl_kf(+t0r8f3oYRrGkCug2mmw@6MPM^DqE+thES^X|MZPwePS&aV+I z&FAX+l@Ket)cq|fZKDoGg@^!Lm%}Ok!+(KyXzJ{`w{6i~E zT(nan7LZRSwC)&bGcMI1+kA1}vT~TOt*dC2YxCLjgRk#|d0z#20gsI9QJSib5cG=S zt8Hoe>4HM%EV4}WF~DD#(B`Ft@T$|pjkeynA{{efT|whuU_(r<V;_I~^G=&H}*y-?n%x8z?v(4>0l#bSm@+6vUmklP$|P3w*?+p?Y7X?9Rid F{|n#@SjYeX literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/dbvis-5-data.png b/docs/reference/images/sql/client-apps/dbvis-5-data.png new file mode 100644 index 0000000000000000000000000000000000000000..fb5ce8b86aa74d48a6139836bb2c2a5e845c9cb6 GIT binary patch literal 99284 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 M`vjr*UT1#ze;WesHvj+t literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/squirell-1-view-drivers.png b/docs/reference/images/sql/client-apps/squirell-1-view-drivers.png new file mode 100644 index 0000000000000000000000000000000000000000..22abbbed741ee2c9f781a31152ba348ef2c5f6d7 GIT binary patch literal 22210 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 TgWch5=t{(qB1AwbB3-0PRa&G3R04867q$Ltc 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 Y@!d0}W_4$rH~(vFc>OZr(#`w-1#~{RR{#J2 literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/squirell-3-add-driver.png b/docs/reference/images/sql/client-apps/squirell-3-add-driver.png new file mode 100644 index 0000000000000000000000000000000000000000..9a9c2c2634e3cbe73d56dd2dfc1a7ff2de9b9828 GIT binary patch literal 17121 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 Kkix6?VE+%re>X?~ literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/squirell-4-driver-list.png b/docs/reference/images/sql/client-apps/squirell-4-driver-list.png new file mode 100644 index 0000000000000000000000000000000000000000..35f389747c9703a4ce4dd83eeed44a24abdaa1f4 GIT binary patch literal 29004 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 HFIWBx(4vv% literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/squirell-5-add-alias.png b/docs/reference/images/sql/client-apps/squirell-5-add-alias.png new file mode 100644 index 0000000000000000000000000000000000000000..d5587060d2eaaf3db4931c0d94f209032efea3c9 GIT binary patch literal 22199 zcmeFZcT|&E_dkl`IJQwlMN|Y71VjWzRJt%0dQ)1!kVFMUgb-Q+p^g<1=>$XsMrk6D zNPq;A03$_8kVp+71f-V~YUqLcfW9;Hy?5R9yZ4WK*SdGTS&Q}XJUP!fd+)Q$XP>iA zB5qh1?%Q*GkC2eiK4YWnRzgBQk%fe|&FuOGcmg-_69q2Ye60+x3SnDLOaM1OyIwKB zA|zB4vv=*z4&Z*bmyv_7kkI~`&A)B+i1*GyLUv}x*RR+FK)Drrdr#Y<$MiNj4_tNr zB6m~l;`=i`bNi#C)s3bJ5vkXsj$TnM&aX7kUk<9FemLo`()|3u@H@w!Z>f>wMS=wd?vFZwhliWbkq=^=u)XQ<_t&sKrHaM!z3D&pOw_7UUn& ze}TV1-egBesAp#5z|~!+o*ck86zrlzyRB~r{s$gN4572{8BeQmkn_4*51#nF9K)oC z$<;HTbFcTdu4*kSZM}Cja3=UX%7mSo9wT?kv*VB>_crAaQ)3yv^{py#X6-kWk!2`5 z%l~HgGr#S2eol>@`ryJ(CRb>x1C^M7AA}-pF)X|4{CD~I{7e$^o#KLoDHa1}l!qT)&V-^S zN8bK;^WFaU7H2}=A1s&^H^l|qOPjXPA-NkOx!k>HNj_88&yiHuJY+#TK-mj-Ds(6f zEfqSCen_BWTiwpieYTHmgA3!50t$2FH!vS&UxgnCju;bW+E)j^t7g6i}iszgs zqP_Bm(RWpX_;~dnguS23+x|lM){H3ZFye4n$4#ijfGDkmc%e3vJeW6_f0nF80!LyX zL3^(S|W1@yYwxqh!vLqZORQ7uNrxu3uXrGnRsN$V{7l zqi1jxwy*kj)x#@Ooxq#r^t-|n!|1Uz!>wsC%AI!x83fcRBK<&9tkVAGKZ&Z(b9+`% z^6`)gn0n>YE0sd_&yrR%N%{-dQM)%OY&bp9i0!R$H}tPluhSj^ALE3O4>e+c-JQ?r z0R)D)GafT}1pO=CbZY=PV@uRXau;VD>#z8@k0`zof6JADd3y%~5ghr2@qEo?FXR1M z=icSlyOz0{Mbq`t@plb!@6IS6o0-%rtuxKlP{$YG@V7 zKl0165~*J1UvE{uy#mOxo_!Io zMrMSJ6&{sz;R=_~d-eh#!qQa2% z{DHkvVK9xUZu%2i+D!Cxn#voA7QH~oQm-4rmwjl{sACys0OP`z&q0$RWMoxV5(G7J zZy+slTtJ#l{_Pf(msH$;B-DGgbYM;@2wXNthV8{&Jmm0kPG&#N=z!rJUI}r zx-dsk=p#{&O|-WfN^zAF8t{n%H9TNui`*B87yj5?3|4*=2U_J8vecd?b)av#!0_pI z$;Gc;E$7{9$S{Jx2CtJQ3}g9u1oT)i!>#*#Z)?nPRZw)C7395Bgy#Ioptz~d2<5f2 zaK!=XerRnQ523z|Qz}k{oO0^PDN(ohd_+{FO75Xz-PZ)#wVF`H;Zok~Ym3FJ?G&j_ zcb)foJbvk2ov8X}2d?HQ&7#>DW=URghe(Q`u)nWMv@gdcAxEA(OYh79qc*rZ)t}(r z5OsrBo~Vb*z}B*+`~3x$17Vb5w}Dic7O_Ed0N>t!LizHD#K0@ME)!DK{OobHaz1MJ zPPkVR-wOKG{bz-w$0H{06K0gELLxZJk!XA|(kO^}mW))jSNznTyh1Nnx#N7lpE{R) zuWGg4IWX+N$tSUHrUOQv<>skcR1wPuzR$6ziJTAPlNo!WH~zf7CPALPbYPa{DnQ&l zgJ0BO2!QadP3Lq85yUTNdk6~d z@vP9E3IbpaN2Hc}7n|Tc9S&Ig@-MQa_;d~rQX4Ascqu1TV7~?-&qyWRUrYGJa9P~h zUwLY6ZuCO6=9Ht@{*6dh4Kssi8qgl9pI+*&u{0sPS0!*sP?Nvm7c-p_Ls1GI+>Y2& zUo}Bvp)Q^k!wQv@bqfz)QC^7)D2Zi3Pz=QNq-WH?kP?~rRwJi#PUgnMDhUG9$JuXb z>1B($)K1aAjJ$?coD7l;SGCSPgs=PtFjL?0Ml4+NVnQhg+ApB?YzSJlpFYl@IB86u z*M3IluEt(P`R~V}M{BCvL&KQUZ?CPN4W7*+KFi0JeTBQ8$&P(~{a0$ez2+I!1CMsQ z&^>8NzCGMd>reOPW#x<)h&3p1iQ+L`-I=wrp>HS1_I=q>PIbTawA>Kiy>eSMbmPUZ zh5dzsvu&RITg14#f;;cc@_j!%kOF8m1FvKtodyfZj&AK4n65F62HsId#d`bLv&a}N zjo_uyup@nCqcPRu>(oQl{X9YVKowUIJG~Mjb);`ajQr$UIx$FNyhwOAZY_aTEe<2Z zFfHIF3W^q<6%EA1Dihrk4iVR#?L7-lyS-86CLAi1=c3{vU*7#gbL(37Ce5=;sr2hT z_fG|*$k}WABr>8y#xeUQe^n)g^_|C$*+zzVm^k<^9Tje4=b|qg8osVL2{>cCO`LlK z$g@4C#YbNsB+2!~@$Ra2;)8}{U7r;9O~3J4duaJ zhT&5XmZ5o0hc9wx!*J>-bh&_4{Y-tWlxgAgZ1_&^(_Uqo0(oeMpELslm?oeTRt?W$ zEWyV*N)CKV!fU@5E7dfXy66JC9_Zc@-b3J`JMT)>XM1XUqDwEsqAv}#90S~Qo|Q&C zc#FE)1g2xPHb7aYTliCJ=uETM{9e>|DLcS z=3^=eq4#m{qzys7D# znK5?EV_(i)k%B{)-R+xteqNto@s^b#`(2;6fkm@UN1iEX&e%sf4z9@@BK2izcAUx@pYygMd~zuc%)5 z0b7htIRD5cJYpf=L3;6(ANZ&F7r;C>G4TcQ|6`++!ruSImHKM~ zZ}j8G2N=$EJ@>Bu!`0^On%B1Ok8}zr$LA7z54314eVZoHE3g?(Dy!azl$scoz4em8 z9``@s-*`aJME6&Z_Px0q&H8#CVq81+2iNHlJv47|^;z-CSo^1)8Mbd+cH2MmwQR{9 zXm3O-sj1kE_14yF(1U`kCRf^XG8Ex3A)$MZkHxV@NY9^hZasWIy!fp+e!Q_w8eeqp z)VYF)nD&c~e}u0Fw69#c)A^eJ_H~--!!y|(yFAZ_F%G9&$ezWUD3nx483Z-ByNr>G z+o1iV^^%FP#I$@NA?u&;9tUSZsJ8gbP&>#$gIJn^ukg%S4-=-E@p+6MvJaeHZ86pE^%=DT1 zM_yzn3^sVaRY;Zy@_PS^fBuMT8q#CAzm>i?sux5qY_Bn~P-yPtiag%<9L@aD^L2v< z?Y)hUi#EFPI@$@%d@Nai>fy0O;TIK7P;26a8O1u|cCHd@%Eq4hO9r^KmOtn@-XRjmR~GLzX$^3bRFF&CdA{ZKjM|)r4vM*Aa zD}SjW+H}K38?;8M5944RC@I%eL8#h^$6rt|!{a-I{y5V7oU>o~M*O7oh@*x_T+1$`?w!L2KiSCuItpGqG|Upzt9&CyxMvEyGm z@w(EU3@oq_PS01RRK#~#FrJa2wzz%-Vy z6jW7~ls@0cJ8E>#yEhQmIQq1&=!6zjDF)Ci<>Mh~FSC-aPI%-8ROV>Z^F#VK`d^;L z!EjTa9(w%p=1JFBrK!@)u_Bfjpy0J@QJnoz@8?x#b|@c=Jt!ioxF3Z+cj8`0*Rjq{ zn?aG%&V2Rd_sl=m>naTu4hWpAd{0mv;tpJnuBoRJKtatFq(i3!cy9jD-TMT@xwd3G znI5MeBfQ?_K%DpTEIm!E5B)7&d~{7AS$MApR~RfaCvo%PMd`dT<}{im5Sba+~r$YoGgfcNm?sxX}i!LYR!3^{jdY_^&$^X z#oa#=E(Jq*D4bsDRy-jRw+E)>3=xr%c+QSKkvd)t{v4eZr5!zU|KqtN5p|j4`|5+= z^R9R6e(spBI^B~|*W?*fH$@XdhwQWh>s3#t?W}PbU2`yDa3$zp>dsF{2`hl@`v<}f zGzYh{hu^qvzjBdyb6%GCUb`**V(}}w3Xj~@OWQ;C?2VpR|Dw4==r=@j1}BMT9aV|G zdwV@7;El#}zY{@r8u<;s>wHi%F!UHA{PJs~56HPziPS9&vv-fqgy1LvF_phOjw8?1 z2jB2N=pp6A!5bZIigP0)9=l+syIuNxKlLxvuKEJBUBY_0zK1eNb9evnm;AMfvus`3 zsb{IZ_D5IR>Cp72!n4y?5-+{QGQewKG^&nQ|!o*HRdb1P=Ub;_9d_8{^~yD zgB{0&gg&1S$AfP`HJZ=d+nDUem>gf(DAbYOF;x3DJ|%8d(RBGYZW1*Ui7=Xj6sXSH z`WGSAakiLO(hG`IlZPEn?#eBF=W+SW6q4`n&yyJNBk!z<9vqeTe4@)}0>{$Ng5g`E ztwNdm>$3X0x%!ez!F|!%u4L{pT2!nQ@13Cd=2P|6OHJoemYnto-EK+Q#@NyDyMoNitWQ2S<-Z6CeHC>U`|Irq^ojC{ z?YHxOM{N_bddT@H@fB0G>9-Gisk;GJ9lOUFP;S5_9{F)i%8O$aw`fIfErP^a-1*mI zGLMv>5j0gne7TuI0RiE3OGgv0kl1%znashzokh3QiNVb5ZcymYn_aPGgL|$TnNt>j zJ(YI>gI5%3)kp0V`4lfD)G)h~A*<$drHV&l?>coe7J|1)xy6vJxq<>&6mTSw9XAJD z^;Q{Q-StNm8qy)@nK++3NzqFHKo}7n) z#O?8PI{GsMqG_x|c1=g$>vrYp-pL5*N{U0{pw|2K)}}wAxIK1pB&fJE9)N92S5T=5 zRwzr&k;BAb%~}7jMgP*7$=n8;ChU_SRQO{K_DxnIr2Odf@nD3baxn@|%nA9k6G|D^%`Wv2h@S1LDnnQ)sg*q2x6~VWi0qp_CEGP$O1?6*W}addXUW44-1KNZx6j-jnPk%hyCtO}PSS zh*>ut>7MHX-z3Hl16lrdDCP205BazZ@wY@gScf3^*zx?YTh0Qlo(5i5A-MW{e&nJ} zDVasL%InTVc+ED-htJXU!cJT9 zNvHSvqz1r#hOjM*8jWU%}B15KcGgW2mbpk?hYOJv(ocN7hu=Piiasm{sh(d}>Y-jR=@JCN2U#_Eu-H z*Vi;6!7yd&tjBRi4(6-ejE7?O>$1vbHi0u&CXAd=#RpaImVfz@(=>C?A@PlWA6hU5 z1)Gbcu>2awv`pcUEaCKy&n|@EqH1lxSdRJAc<1+t4a^QQr9?mcY_HN#Um*VSNFmc6 zrrTrTH^WWF?QvIGd2ne@S1K{{hCoEMxS8F5J7_)h6nWXFQ8u}3|66(IE+5-d z*P!LKV7aNNCBdM6t=$f~?&4&$>A94%Zx7=1;8`|9ou%CR5mcXPY8(Vm^T=M(Ns-;_ zOZ9qC-KGW{y{jziAS7Z>fzD?cGQn(2-|rGM2(DU+*H&J*`XE!xslo^&MnQ_NiV@fA zOg$ft!xGbGGoTm!K6UOLK%z(7@1`I{r*$^zMY~-JT%DLdp zCc9^iU5FV%9A^V?{}i#PlZqR7KVDcYb|npMMmB( zVkp8gafLzs4Wv5Pny-b%fHvG!+Nklp2}$Wt>1Sq6eVP=A@fkcZyy>V0Y`FS zth=#Hi#eB|>L7>oD3?!O5NqZ5#z{u#>dn+1s$sZhT(-$I*O_?fv?@DC2(_k=dU8OL z$#uu%$@qTh(!RnyXGzE7AZRlMvQJ>@8+PPRh>I6_bqi zDf?X;@fmPGn;gl#VZdyyWfp>u&=tqN=$9)ucvc2fgXKx_#?GKR!U-2lJ#_dQ{SYLA zo?}jDp6-d}>`%jG-(ILCj-J42SJ`@Hw|qwC>>CTQ^)sU=-5t%o<0)c@AA2La4^rgsTq zwzmsm;z%dBQjrdhM_dmwFivf!$v%k9xwbFrY$)8f0PQ2(EW*^2;viAyQr#J%dG|?C zpZeNe55fNY3~4y-LQiy`RQ>}wGJ`LJY9f>ykXKN1pZ6dV#!W~~Sd3K~J%G~cdk+<{9Kp?*@ zpAD>}nn@~B%-<=-EgGzaiWG^SFWwMUSxmh(Aj-g~Iw-52Zlfji>RpwM>)3%Yk9dCQJHkyj*>YG3N|$kbZbMhk-S)Q~pSw`$mL=@W`m>`M;pM+C12DKN(?*;hd>*bczbQqZDWiNW~4~rFYE|a8*R?6?>WAY48 zL+cH}j#UbTsDR6?XF3QbPlJIO>aYd&8D$8kpUl4`Je0gL!O~_xma`-bwv5q&6SY3F|w= zTlbkupYF0ncj9L+15WLd%f0?*Ot`BryIY?Rt3_b1_leJ}>StEHJQ9h${zNH> zMK~zWEP1CuW3RN9G)EFxB^Ie#uj_y?@lNLhKvnb~^>>_!^WE7Pmf9kx1suB-A((Qr z58wNMDPqq?D^}Fb60^r&3@=E}X$5iw$L_2KSK-}r|5jv(Gh=YVERWuiJ;s;KW73K3>K+;l` z#}S$i<#F`pEz%vD$5LLTf6l+aHAV_vJ*&;EK+bf*6aELHYn%s`8ES>~DC*&CLy}n` z=Y@@zc+JTMUej*8>E@!u+DY=4ft5IA0KFUpfME2lBMzD~P#M*XxV>7ZYGW4DPn|~Q z*nMBK*mX?o@1=?y0~Aa!3EJAFIMAk5ZPUnG9?PgYEi(96-{?bkhFZnQCYW^yPRvVS z_1ZH+c(ntc>v#i64>e% zT24`5K=`0sxtJ1mBde~I-UPG5X!JCd1*mLXf=#(h<`pO-v4)u}{BIDndrUjuz4 zXonrq^{tB($lx`t)R-#JN9-m z3|ocxL}W%$xqRu~0d&O(E$H*r0A9FyILnN21AG$_V$pW`NEfIg$a<-sjZhj{VD^(` z)`oNalVvDlVWaK?lWb3AD!WTUY>VUxfaJC_SnI|2QRG36hfGRNn?yLL||ouifwpoyP1RnlGy z8092+?$DOp)vln#A8}eNum4WISHzayQu3`w@o%mGS)|bHV?c5isNKduf+-|)QU%~# zhaswY>a@$xA%{$2Awoib_?;y0n{fsSA32pL-HqOAwke<_QxtBE$6^e^W6mLd){fbf zT-J65yuqi5NA%pF4+B}?z5hIS!aDme*6W@+Q8_R_yS&{$z-vi(vD{K)zJV{pzeqhr zHk3df#*=H~oOqbrrm}?{Kl%gS0}b0r>Y*ks4HO0G$vX1CXjdZ;tJ=U@4ZQoCiJ(_^ zPe`vzI}iU~B|4Y<(_63d;Wgdb(G+>}0Q3&AyRxmt>hu<|ke$G6ZSdQ1#&T zXHw4$xij5fpY$BX*6B(c^1X|dRU=)PljKcUS32;bW?^@oUf~l^ZJ9DuFk&Lk5Wtcv z@0|AUT^X_R+YqRO#hh;)j!jhU{W5kq)f)l`RuK+ z-dHN%BKf49A41a??#@s;n_S=GGn(R>#6`2u8TK^sRHg(-y{hFeRpF+V0oTjsuf?1J zLW^u4$$M1{8gNIUT>g#M)umDODhJ6!XAcb^OG{v)boll2J@hEdG(@}_EnG;@rZ5#0}qQ-4$MKgwzK9i?d zBOp|TZS=A9R`a(PuRnTi#8VV4b`&(_w;-WTKP4$We&-iiZo88RqjQfWYJagCG# z+gRzZsgLO{YQ}{HTWH_{bfzwAlh~iR2nJrZH+b&LzycE?478$C1;l@dd@=4xnO&*m zgf5R3Hcl}r247sLxf5QfB=bm7`n4$vNKG`Ii57m@z4=mxE+CPa80ulnH!M((0?v^q zo+ZVD0}pZv^JknF9m1hU)91P(tHaQ%pQmB{r3#d9)0R&KF=eMuc+F?u;d*E6Q`z`y zth#e!Ez|2WQ|f3aaxQLUNVv+MYor}tCQu2Ti5%#0$L;H1Zt*u65asxMHzr?M^4ap0 zCCpTRni;Ba7+9gOeE~T~j`t>BxCqp9xBx{eU$*U(_=fG*M~Nwt4vI44;#lv^Vtqs) zQS|g=dldY-AMVu+)W$+QxaL%9*nvLOa!mD2P@kmk(#VEHOBqjo;9Xedh2RA+xTdpz zRd0YK)hn?+w&4%f;C~4_z$;%=AGjb!3CfZ%?p^N$!6Yg-1XJNtAyYvknYaJ38y3Gc zX0H@$zRn7T6MUjoi^vCG)R}3t^kj){Xl4_^RRaM@Bn|QhgC}Hv5mfMGj>J}({ z5BeLs-JYz>U3tV%FvI*5DL&vZ(ajkYr*$ZbXW#~KWQn52v50&FRR8kgz|-&$RZiHj z?7;8G7$H-xHHx~^<XlJ@j6VH;jZvgfp zcrX*8A{91EeNbsO<2cE>r4#qYzsp;OYc#l!vC@(IY)p5td|)`iQhAE?Z`IP=aQwIn zi-AS}g>TrGg8=5E5G(J!;k*cV&_$_`FRRN`${0?Eh%_tixv6rFnAltQ0_&^*WDzif zL!U5)U$PcC>AoCm6GlaGY){4!&Djw7r^S{*bR%6(3| z1b=>HV`KKvLKlju0>VF*IH{5|vgZ5AE>Pc#pR@yUm~NWD=}E*h53zDu9aFU!m)Qlr zio2DrFCGy=mJ6ff-YGh~h=Wvwe0ov1>!Zn37Cxv+#By(Lda!d`X47*{A3uLkC(`G^ z&8g9i5X+R9^w6~}Gs{>TmLC{So;s+z(zm^)N32j`k*RFB8SL85gYe{zFy#Ov)MDdF za%%$?WchUriC1L57|uBiiFnQh7LYU(J7mZQ;~4f2D)6KLJb68hcFqvBQF!21<8G-y zL>V}JLn|?6C0IaCXC$-QEmpr7X&=N1Vx-m@ylBK|MVW4wzoS6%3T;Q^UlW4zAy3#> zAgL|4z4FAy+MoSMns%9i(PM#6&??6t1bJ5&OW#!-{1CSc41L`40rNSwHla1MpMX#x zSDkrcH`_O)*9`=u4-G4+mbZDpv5cKzG4fti$m%A?h z@g*1*LFC1dt!^M*k$T;ee{)zJo1ofvANM5-Pkt4k8rTh9&q59&hXS~WfPwu$b~L>Q zb!-c+MwiF4=oLfnXDeBE1?x^N%%gI~$_&h|nFT)w&&JO0u>XswrGa%0R&e16O6gG5 zcc8o(`bLslD+7jNOg~}t&K3<6MAlHD$md{?syncBOyRcp0>I(YjQbYS%G!+YhTa=P zf-sff3Fd~CE*)`yS~YUBC7kBI#Sp^<>%6$QF}-bMNJ?*tR?{k%3d}QZV!a(hf;Xnhq{=Ghp z=Nhs4qeH6pl3RRInzK2flhpoQwBUsd3`|=tlMIz;ENJjF^InT(iT<=jX>xfSD+&aa zLWRr^H8GD2tK^BopTs}{&6mYGDJ$^~7|4|c*iY20t=rh-@>qySZ@BK@%@L{ivb79P zCF*Mfze^3u!@qnd#jyeVyl`z>gnz|GO#}9=M)a;J)vbhJ*hSeHG`k2}8T&|o?fMK( zSmsV^GXjA0C&{Qz&5$CGs56YuBJq;PV*4xD@FZLo{vGdl*21 zM0IS!esO`3>y(mDl4HfcqfB2FviTL%DfKNmL$~nif4%~cWh|s&-xjxGK-Tm(wR`c* z1I}VT-|5@IfbYWZE&mQ-f!yh5#?#)d3=R!|Th%Xk@Ch0Gqs)JvaPi{Rh0Z&20^w)B zlMGYLEN;@{P2GJZS3HuFMv$269N#n_$r+YT`E) zo4T0G5l&COLUeb;4Hb`O-vJ5+rQ8+^|4mk|7dyT#ep`zpxywBM^m5(>zo2lqQ`zOJjT+*MTD{vwDO*N5 z3<$fSxzTe-P>%Vy|GH{??H83O1MMNLn*uvFvzE>Dk%`yW-tLWMJnJ~mryo9;&y#K&-2qk?HWS1Dy;c2V?pwC=|CI6m zzdj5QFLg)H$Ly|`3JYEi4wpdfS}do9RErp%IB#Y7_#YpK;k@W=8p_oZOnjt6gPw{b zDCa%MT*78n_m4wugw{%7{&4wWOOB1$=RC2hrMgN^pI^Iq z?`I+E5A`UIhhQ8-D9fY3ZjZg)52uge@XE*1T_oaIV9#<{kM57Bbe0<}QEXt<;D`TC z2HtD5PM=Nmxd_4oLMuo9OIE#&p8t*rvSg+w86NOyFZ4j=N+*s)1@Q;v09b)Ox|xr!^2msd)MJ$Y(TRFrfRo1B zbWP1;$@xEhoI&Z-`#2EjAB_W4!IQ{B4g9fv|F?9A{rMk;rO4nO|7QdsGH#f>?#&Pn zuln z_#SOunUiL`;v$b%(rz(;rz-ohAN;GwSV*3A!6Dtb6xx$iM5Si04^?Skma3vcRWAGX zlfMK|6YwKGs*2&sHT$)nYyxtb5orqkoHG4iaydozVLo~}`gugPiTW?q0r;#$eo{=n zsC4b`NIhzRVplgPpOgO{5=XD12e-eiO5y=0~9L84o0;W>=$K)k)Mgco1|L!(b z58=s&MPeaF-yuwLUNS2iu)vNVx)jJLjgQrCP|o@e!2nPLAe{f+{8tA5)r0@qf>6VM zW$<4aY-Y{>)r0@LGRQIB+Axln3yiXjWxamSIT){cH8`Dt-mHPoig?87`uU{T8&@FR zGp+B~gfGChE(q9B=H7p0Uev2ZjMIWrNL#g#&M4r#2l$-35|-eZy7K+e3Q*ezPK-H? zJ(LT-yxHRcRdJwT9s|4ay?!#wG{*#0f%B2xYFW!jKiU$lRFXtcH+Bxgh?9ZpR13G=lq_6b$v?995e;} zaXx2xpN3mMH&F4;hHH_qIk!`(eYS2v)_+yCabpT*L1r6>O#5a>)`+u z)Nx|rn-I4LLA^zjs-1B8vC4@KlsKtsXp%XCq*KDX<6-AAuzX9%E6Tcl5j#vE_+x@GLMOKca7)PB3(ku^~CL}v+Ccv z0i%tj`z&N*xoY(vs3mXei@y-$S^d4Mw)FKZ<`-5!*7{o5$$iE0WYvj?%$U9U!}~m_ zL+T|hY}KgddTu}hWNco#Czr3!QFT*PUYvZ8k~UcB=Fm$lOQ$E9d0$SUFE~%O`J+4V z#Klo$n@_~^XT(anSPDh=zSI!CrmLO)JKJdTv|&;s%DF9c{Q1Mv&?;V8gEN(jKFR)! zM9xNso88J8A@$E>!P%;;od`%o5}mKDYf;W5CJ-`d+&~D?DW=MTRH-|u4m4!D8<~8qgD_~ zql{M%SkBA_NTg8UJ&jBYN_2}mM0+B*HyYrx)|wLC!EbVb%7Jsn3@G~|(!+bwbtY9; zm!%MfRLX)!2)ZPJm6YaDe%aMSe5fT`G_>N;MPSjY>9l-gdh9|}7IGx3%?F)`PRXjY zxTWVQpPm&Bxkq%8XGIfotsxFN4Bdv*CafzFF~(kzPGF^X<>&Wz_@{6J7s_>dDmx3O z__FDPy4@%?A=71cyi_59o6r%5Rs!lJokN(0X<3eopV*HHV}**=lxBA-K_;fpIoUX< zkAw;vUTa;My}!yeFK5yfMo=DLR2#oy!6lylJ9VUJ;zSg)C_%a1mdqf12NJGvxd5~w+{B%P*jG6I6cU8Jn(eQ}{I3;j~P=Zb4%qqi) zg|5Kf#t0HlFH}q*MmN(j)I@eq)*KSf(DD|LP=h2SwbYR?gkho)AM%~3r6Ey2NJq2O z2KhtW?h&ZeAgY3UvE$*U7EMN|@=mvptsGUM=V2Y?5GhHGnQx|N?^C&1h&|e9ml+ZP z4)zR{J|~YzRgF?4a+yBbjL@DD!ajhzi4`P23y_3kKe)qy~a5Q-#wB^z`Dy%4VNUGD`wvhK?L3MBr3Q_-}FxDC{Ey}F0k0Ci_hA} z@MgI=AyG{DBsq~m;F4yKN~Hz}5MpkVkro7D0w6?fLUda4*n&nw*4*qF>JUF0o)SYX zwN!0%f=F@D_lPRAilIBqHk3yYVP6{Eg(R%nDRDsov@O|Zs$0cnRcpdK$*qVP!EQPY zH%SX{fRJb3ZxRMbyit{0+0ibQlL}X1fZ-zCwh3e zw8@25cT+iLx^4_{07Lb4YgbNg^vvZ2pt%j1X@f|=w%LI4%Pk8>AEeZ>aXLLigN0SK zbj7V0@g_jc=iPOAGfniIRlmipoVb;lz?s81r?B$|eZ9AoX{EhgboB(#z1#rpTSpen z`>qqxmFaxs3uZzfp+Y$%*_f>^cvPL;O2JO?_2qEFgcz+lc74?wXL>71tI=2Rt<_14 zDBqgLD|^X)oY=S?$fRGC5LU=%?j$!?t}ncDOQ~H@gvzm1kgKTja|wU|O_x}2g6`Mtu;Ppu3j=XM-|eaG(n)?%V^NB5(`I=oKlQD4x7l(9rs!7 zc~B1NS8flbo))yL;<(1Ew_!WA>$r7rw-h4Q0y*Xl*uR8!9dn^4B9|00-_tg`gr)!* z7y|DBDkdyYP&O418S3InWUcDNf-DLK51DejHLxkId8+3Xq{(mP3BlcssnL1N;gH{z z=p8vKvsz;u1aBcF$EnF%a{5?Zt&R4T&J~oc2WDwGt1aKFLPl*5Uc2U|2I`4fIt#XuJq^NX z;E`FC^yo*Nct?jue7>~%h+PC#P6P_jQgkzxbu7BNNM`Ppb);{VIRyzv=`BSL$l|o* z*4opmj*I8X^D9t^ZWtERbM=nK+Dn8jm%;}R{FNVOp9Gt0gr+q1WfUYunKDLKs|K*X zykoZLl)FLUVFJRZK-dPfo0+(eZ{DLdv??G8kl6JWBe1P@9V*a8pd}ZSF;n671^yV0 z8!qr;*y>F1lT0ZD${EM0=TShJn%CiIiIn<)yX-nJ{oW!SD!D&^>z&=azLu=dTee-& z7eTKrtAs&xOq|#01Aoyg)j2b-sjMwptseAmqmO6PMI^jS^Vi7*IQ@sc70g)1L@t@>h5p)QCsSG}!w zVeoIF5=lLaP!bEap%OWx`!hEMZk-vTHcS?}S^Hk!9D z

}gL!GJ#h7IreU$SGxR0S=`Br1KB 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∋Qet@idqx$8k) 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 Nx~le#yz3T${|ieXV_pCN literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/workbench-4-data.png b/docs/reference/images/sql/client-apps/workbench-4-data.png new file mode 100644 index 0000000000000000000000000000000000000000..602f09d06e46ff252cba0b24adad1f9fe7bb21de GIT binary patch literal 75863 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 + +==== New Connection + +Create a new connection either through the menu *File* > *New* > *Database Connection* menu or directly through the *Database Connection* panel. + +image:images/sql/client-apps/dbeaver-1-new-conn.png[] + +==== Select {es} type +Select the {es} type from the available connection types: + +image:images/sql/client-apps/dbeaver-2-conn-es.png[] + +==== Specify the {es} cluster information + +Configure the {es-sql} connection appropriately: + +image:images/sql/client-apps/dbeaver-3-conn-props.png[] + +==== Verify the driver version + +Make sure the correct JDBC driver version is used by using the *Edit Driver Settings* button: + +image:images/sql/client-apps/dbeaver-4-driver-ver.png[] + +DBeaver is aware of the {es} JDBC maven repository so simply *Download/Update* the artifact or add a new one. As an alternative one can add a local file instead if the {es} Maven repository is not an option. + +When changing the driver, make sure to click on the *Find Class* button at the bottom - the Driver class should be picked out automatically however this provides a sanity check that the driver jar is properly found and it is not corrupt. + +==== Test connectivity + +Once the driver version and the settings are in place, use *Test Connection* to check that everything works. If things are okay, one should get a confirmation window with the version of the driver and that of {es-sql}: + +image:images/sql/client-apps/dbeaver-5-test-conn.png[] + +Click *Finish* and the new {es} connection appears in the *Database Connection* panel. + +DBeaver is now configured to talk to {es}. + +==== Connect to {es} + +Simply click on the {es} connection and start querying and exploring {es}: + +image:images/sql/client-apps/dbeaver-6-data.png[] \ No newline at end of file diff --git a/docs/reference/sql/endpoints/client-apps/dbvis.asciidoc b/docs/reference/sql/endpoints/client-apps/dbvis.asciidoc new file mode 100644 index 00000000000..dabee6430fa --- /dev/null +++ b/docs/reference/sql/endpoints/client-apps/dbvis.asciidoc @@ -0,0 +1,42 @@ +[role="xpack"] +[testenv="platinum"] +[[sql-client-apps-dbvis]] +=== DbVisualizer + +[quote, http://www.dbvis.com/] +____ +https://www.dbvis.com/[DbVisualizer] is a database management and analysis tool for all major databases. +____ + +==== Prerequisites + +* {es-sql} <> + +==== Add {es} JDBC driver + +Add the {es} JDBC driver to DbVisualizer through *Tools* > *Driver Manager*: + +image:images/sql/client-apps/dbvis-1-driver-manager.png[] + +Create a new driver entry through *Driver* > *Create Driver* entry and add the JDBC driver in the files panel +through the buttons on the right. Once specify, the driver class and its version should be automatically picked up - one can force the refresh through the *Find driver in liste locations* button, the second from the bottom on the right hand side: + +image:images/sql/client-apps/dbvis-2-driver.png[] + +==== Create a new connection + +Once the {es} driver is in place, create a new connection: + +image:images/sql/client-apps/dbvis-3-new-conn.png[] + +One can use the wizard or add the settings all at once: + +image:images/sql/client-apps/dbvis-4-conn-props.png[] + +Press *Connect* and the driver version (as that of the cluster) should show up under *Connection Message*. + +==== Execute SQL queries + +The setup is done. DbVisualizer can be used to run queries against {es} and explore its content: + +image:images/sql/client-apps/dbvis-5-data.png[] \ No newline at end of file diff --git a/docs/reference/sql/endpoints/client-apps/index.asciidoc b/docs/reference/sql/endpoints/client-apps/index.asciidoc new file mode 100644 index 00000000000..ee9891040d0 --- /dev/null +++ b/docs/reference/sql/endpoints/client-apps/index.asciidoc @@ -0,0 +1,21 @@ +[role="xpack"] +[testenv="platinum"] +[[sql-client-apps]] +== SQL Client Applications + +Thanks to its <> interface, {es-sql} supports a broad range of applications. +This section lists, in alphabetical order, a number of them and their respective configuration - the list however is by no means comprehensive (feel free to https://www.elastic.co/blog/art-of-pull-request[submit a PR] to improve it): +as long as the app can use the {es-sql} driver, it can use {es-sql}. + +* <> +* <> +* <> +* <> + +NOTE: Each application has its own requirements and license; these are outside the scope of this documentation +which covers only the configuration aspect with {es-sql}. + +include::dbeaver.asciidoc[] +include::dbvis.asciidoc[] +include::squirrel.asciidoc[] +include::workbench.asciidoc[] diff --git a/docs/reference/sql/endpoints/client-apps/squirrel.asciidoc b/docs/reference/sql/endpoints/client-apps/squirrel.asciidoc new file mode 100644 index 00000000000..c5a30ab15c9 --- /dev/null +++ b/docs/reference/sql/endpoints/client-apps/squirrel.asciidoc @@ -0,0 +1,50 @@ +[role="xpack"] +[testenv="platinum"] +[[sql-client-apps-squirrel]] +=== SQquirelL SQL + +[quote, http://squirrel-sql.sourceforge.net/] +____ +http://squirrel-sql.sourceforge.net/[SQuirelL SQL] is a graphical, [multi-platform] Java program that will allow you to view the structure of a JDBC compliant database [...]. +____ + +==== Prerequisites + +* {es-sql} <> + +==== Add {es} JDBC Driver + +To add the {es} JDBC driver, use *Windows* > *View Drivers* menu (or Ctrl+Shift+D shortcut): + +image:images/sql/client-apps/squirell-1-view-drivers.png[] + +This opens up the `Drivers` panel on the left. Click on the `+` sign to create a new driver: + +image:images/sql/client-apps/squirell-2-new-driver.png[] + +Select the *Extra Class Path* tab and *Add* the JDBC jar. *List Drivers* to have the `Class Name` filled-in +automatically and name the connection: + +image:images/sql/client-apps/squirell-3-add-driver.png[] + +The driver should now appear in the list: + +image:images/sql/client-apps/squirell-4-driver-list.png[] + +==== Add an alias for {es} + +Add a new connection or in SQuirelL terminology an _alias_ using the new driver. To do so, select the *Aliases* panel on the left and click the `+` sign: + +image:images/sql/client-apps/squirell-5-add-alias.png[] + +Name the new alias and select the `Elasticsearch` driver previously added: + +image:images/sql/client-apps/squirell-6-alias-props.png[] + +The setup is completed. Double check it by clicking on *Test Connection*. + +==== Execute SQL queries + +The connection should open automatically (if it has been created before simply click on *Connect* in the *Alias* panel). SQuirelL SQL can now issue SQL commands to {es}: + +image:images/sql/client-apps/squirell-7-data.png[] \ No newline at end of file diff --git a/docs/reference/sql/endpoints/client-apps/workbench.asciidoc b/docs/reference/sql/endpoints/client-apps/workbench.asciidoc new file mode 100644 index 00000000000..e50a086ab3b --- /dev/null +++ b/docs/reference/sql/endpoints/client-apps/workbench.asciidoc @@ -0,0 +1,40 @@ +[role="xpack"] +[testenv="platinum"] +[[sql-client-apps-workbench]] +=== SQL Workbench/J + +[quote, https://www.sql-workbench.eu/] +____ +https://www.sql-workbench.eu/[SQL Workbench/J] is a free, DBMS-independent, cross-platform SQL query tool. +____ + +==== Prerequisites + +* {es-sql} <> + +==== Add {es} JDBC driver + +Add the {es} JDBC driver to SQL Workbench/J through *Manage Drivers* either from the main windows in the *File* menu or from the *Connect* window: + +image:images/sql/client-apps/workbench-1-manage-drivers.png[] + +Add a new entry to the list through the blank page button in the upper left corner. Add the JDBC jar, provide a name and click on the magnifier button to have the driver *Classname* picked-up automatically: + +image:images/sql/client-apps/workbench-2-add-driver.png[] + +==== Create a new connection profile + +With the driver configured, create a new connection profile through *File* > *Connect Window* (or Alt+C shortcut): + +image:images/sql/client-apps/workbench-3-connection.png[] + +Select the previously configured driver and set the URL of your cluster using the JDBC syntax. +Verify the connection through the *Test* button - a confirmation window should appear that everything is properly configured. + +The setup is complete. + +==== Execute SQL queries + +SQL Workbench/J is ready to talk to {es} through SQL: click on the profile created to execute statements or explore the data: + +image:images/sql/client-apps/workbench-4-data.png[] \ No newline at end of file diff --git a/docs/reference/sql/endpoints/index.asciidoc b/docs/reference/sql/endpoints/index.asciidoc index dc4289aef92..59c397f97aa 100644 --- a/docs/reference/sql/endpoints/index.asciidoc +++ b/docs/reference/sql/endpoints/index.asciidoc @@ -2,3 +2,4 @@ include::rest.asciidoc[] include::translate.asciidoc[] include::cli.asciidoc[] include::jdbc.asciidoc[] +include::client-apps/index.asciidoc[] diff --git a/docs/reference/sql/endpoints/jdbc.asciidoc b/docs/reference/sql/endpoints/jdbc.asciidoc index 6a8793f7e24..a8a866ac93c 100644 --- a/docs/reference/sql/endpoints/jdbc.asciidoc +++ b/docs/reference/sql/endpoints/jdbc.asciidoc @@ -3,14 +3,20 @@ [[sql-jdbc]] == SQL JDBC -Elasticsearch's SQL jdbc driver is a rich, fully featured JDBC driver for Elasticsearch. +{es}'s SQL jdbc driver is a rich, fully featured JDBC driver for {es}. It is Type 4 driver, meaning it is a platform independent, stand-alone, Direct to Database, -pure Java driver that converts JDBC calls to Elasticsearch SQL. +pure Java driver that converts JDBC calls to {es-sql}. +[[sql-jdbc-installation]] [float] === Installation -The JDBC driver can be obtained either by downloading it from the https://www.elastic.co/downloads/jdbc-client[elastic.co] site or by using a http://maven.apache.org/[Maven]-compatible tool with the following dependency: +The JDBC driver can be obtained from: + +Dedicated page:: +https://www.elastic.co/downloads/jdbc-client[elastic.co] provides links, typically for manual downloads. +Maven dependency:: +http://maven.apache.org/[Maven]-compatible tools can retrieve it automatically as a dependency: ["source","xml",subs="attributes"] ---- diff --git a/docs/reference/sql/index.asciidoc b/docs/reference/sql/index.asciidoc index 33b9da9fab9..aa9eebea7b7 100644 --- a/docs/reference/sql/index.asciidoc +++ b/docs/reference/sql/index.asciidoc @@ -36,6 +36,8 @@ indices and return results in tabular format. SQL and print tabular results. <>:: A JDBC driver for {es}. +<>:: + Documentation for configuring various SQL/BI tools with {es-sql}. <>:: Overview of the {es-sql} language, such as supported data types, commands and syntax. diff --git a/docs/reference/sql/language/syntax/describe-table.asciidoc b/docs/reference/sql/language/syntax/describe-table.asciidoc index 66c1829c14f..ebefe9bc34b 100644 --- a/docs/reference/sql/language/syntax/describe-table.asciidoc +++ b/docs/reference/sql/language/syntax/describe-table.asciidoc @@ -6,9 +6,12 @@ .Synopsis [source, sql] ---- -DESCRIBE [table identifier<1>|[LIKE pattern<2>]] +DESCRIBE [table identifier<1> | [LIKE pattern<2>]] ---- +<1> single table identifier or double quoted es multi index +<2> SQL LIKE pattern + or [source, sql] @@ -16,6 +19,8 @@ or DESC [table identifier<1>|[LIKE pattern<2>]] ---- +<1> single table identifier or double quoted es multi index +<2> SQL LIKE pattern .Description From b3071133d4f66c1715d496c76909ff576619a65e Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Thu, 13 Sep 2018 11:18:03 -0400 Subject: [PATCH 55/78] TEST: decrease logging level in the flush test Relates #31629 --- .../test/java/org/elasticsearch/index/shard/IndexShardIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java b/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java index 715860e6ffa..cd6a9b27b15 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java +++ b/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java @@ -412,7 +412,7 @@ public class IndexShardIT extends ESSingleNodeTestCase { } } - @TestLogging("_root:DEBUG,org.elasticsearch.index.shard:TRACE,org.elasticsearch.index.engine:TRACE") + @TestLogging("org.elasticsearch.index.shard:TRACE,org.elasticsearch.index.engine:TRACE") public void testStressMaybeFlushOrRollTranslogGeneration() throws Exception { createIndex("test"); ensureGreen(); From dcbbaad296f6955bdb61cf461943c3fac2984d11 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Thu, 13 Sep 2018 11:53:43 -0400 Subject: [PATCH 56/78] Mute testRecoveryWithConcurrentIndexing Relates #33473 Relates #33616 --- .../src/test/java/org/elasticsearch/upgrades/RecoveryIT.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java b/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java index 062016909b6..7bd5cc3a8d2 100644 --- a/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java +++ b/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java @@ -111,6 +111,7 @@ public class RecoveryIT extends AbstractRollingTestCase { return future; } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/pull/33616") public void testRecoveryWithConcurrentIndexing() throws Exception { final String index = "recovery_with_concurrent_indexing"; Response response = client().performRequest(new Request("GET", "_nodes")); @@ -183,6 +184,7 @@ public class RecoveryIT extends AbstractRollingTestCase { } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/pull/33616") public void testRelocationWithConcurrentIndexing() throws Exception { final String index = "relocation_with_concurrent_indexing"; switch (CLUSTER_TYPE) { From 9600819cef27ea3dd7fe5fe93c228035e13bb320 Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Thu, 13 Sep 2018 17:13:36 +0100 Subject: [PATCH 57/78] [HLRC][ML] Add ML delete datafeed API to HLRC (#33667) Relates #29827 --- .../client/MLRequestConverters.java | 14 ++++ .../client/MachineLearningClient.java | 50 ++++++++++-- .../client/ml/DeleteDatafeedRequest.java | 80 +++++++++++++++++++ .../client/ml/DeleteJobResponse.java | 63 --------------- .../client/MLRequestConvertersTests.java | 15 ++++ .../client/MachineLearningIT.java | 20 ++++- .../MlClientDocumentationIT.java | 63 ++++++++++++++- ...s.java => DeleteDatafeedRequestTests.java} | 28 +++---- .../high-level/ml/delete-datafeed.asciidoc | 49 ++++++++++++ .../high-level/ml/delete-job.asciidoc | 2 +- .../high-level/supported-apis.asciidoc | 2 + 11 files changed, 297 insertions(+), 89 deletions(-) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteDatafeedRequest.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteJobResponse.java rename client/rest-high-level/src/test/java/org/elasticsearch/client/ml/{DeleteJobResponseTests.java => DeleteDatafeedRequestTests.java} (51%) create mode 100644 docs/java-rest/high-level/ml/delete-datafeed.asciidoc diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java index 09c587cf81f..81b1f6b5709 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java @@ -28,6 +28,7 @@ import org.apache.http.entity.ByteArrayEntity; import org.apache.lucene.util.BytesRef; import org.elasticsearch.client.RequestConverters.EndpointBuilder; import org.elasticsearch.client.ml.CloseJobRequest; +import org.elasticsearch.client.ml.DeleteDatafeedRequest; import org.elasticsearch.client.ml.DeleteForecastRequest; import org.elasticsearch.client.ml.DeleteJobRequest; import org.elasticsearch.client.ml.FlushJobRequest; @@ -195,6 +196,19 @@ final class MLRequestConverters { return request; } + static Request deleteDatafeed(DeleteDatafeedRequest deleteDatafeedRequest) { + String endpoint = new EndpointBuilder() + .addPathPartAsIs("_xpack") + .addPathPartAsIs("ml") + .addPathPartAsIs("datafeeds") + .addPathPart(deleteDatafeedRequest.getDatafeedId()) + .build(); + Request request = new Request(HttpDelete.METHOD_NAME, endpoint); + RequestConverters.Params params = new RequestConverters.Params(request); + params.putParam("force", Boolean.toString(deleteDatafeedRequest.isForce())); + return request; + } + static Request deleteForecast(DeleteForecastRequest deleteForecastRequest) throws IOException { String endpoint = new EndpointBuilder() .addPathPartAsIs("_xpack") diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java index 79f9267c94d..4d2167ce063 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java @@ -22,9 +22,9 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.master.AcknowledgedResponse; import org.elasticsearch.client.ml.CloseJobRequest; import org.elasticsearch.client.ml.CloseJobResponse; +import org.elasticsearch.client.ml.DeleteDatafeedRequest; import org.elasticsearch.client.ml.DeleteForecastRequest; import org.elasticsearch.client.ml.DeleteJobRequest; -import org.elasticsearch.client.ml.DeleteJobResponse; import org.elasticsearch.client.ml.FlushJobRequest; import org.elasticsearch.client.ml.FlushJobResponse; import org.elasticsearch.client.ml.ForecastJobRequest; @@ -204,11 +204,11 @@ public final class MachineLearningClient { * @return action acknowledgement * @throws IOException when there is a serialization issue sending the request or receiving the response */ - public DeleteJobResponse deleteJob(DeleteJobRequest request, RequestOptions options) throws IOException { + public AcknowledgedResponse deleteJob(DeleteJobRequest request, RequestOptions options) throws IOException { return restHighLevelClient.performRequestAndParseEntity(request, MLRequestConverters::deleteJob, options, - DeleteJobResponse::fromXContent, + AcknowledgedResponse::fromXContent, Collections.emptySet()); } @@ -222,11 +222,11 @@ public final class MachineLearningClient { * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @param listener Listener to be notified upon request completion */ - public void deleteJobAsync(DeleteJobRequest request, RequestOptions options, ActionListener listener) { + public void deleteJobAsync(DeleteJobRequest request, RequestOptions options, ActionListener listener) { restHighLevelClient.performRequestAsyncAndParseEntity(request, MLRequestConverters::deleteJob, options, - DeleteJobResponse::fromXContent, + AcknowledgedResponse::fromXContent, listener, Collections.emptySet()); } @@ -492,6 +492,46 @@ public final class MachineLearningClient { Collections.emptySet()); } + /** + * Deletes the given Machine Learning Datafeed + *

+ * For additional info + * see + * ML Delete Datafeed documentation + *

+ * @param request The request to delete the datafeed + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return action acknowledgement + * @throws IOException when there is a serialization issue sending the request or receiving the response + */ + public AcknowledgedResponse deleteDatafeed(DeleteDatafeedRequest request, RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(request, + MLRequestConverters::deleteDatafeed, + options, + AcknowledgedResponse::fromXContent, + Collections.emptySet()); + } + + /** + * Deletes the given Machine Learning Datafeed asynchronously and notifies the listener on completion + *

+ * For additional info + * see + * ML Delete Datafeed documentation + *

+ * @param request The request to delete the datafeed + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener Listener to be notified upon request completion + */ + public void deleteDatafeedAsync(DeleteDatafeedRequest request, RequestOptions options, ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(request, + MLRequestConverters::deleteDatafeed, + options, + AcknowledgedResponse::fromXContent, + listener, + Collections.emptySet()); + } + /** * Deletes Machine Learning Job Forecasts * diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteDatafeedRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteDatafeedRequest.java new file mode 100644 index 00000000000..1454bb590c3 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteDatafeedRequest.java @@ -0,0 +1,80 @@ +/* + * 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; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; + +import java.util.Objects; + +/** + * Request to delete a Machine Learning Datafeed via its ID + */ +public class DeleteDatafeedRequest extends ActionRequest { + + private String datafeedId; + private boolean force; + + public DeleteDatafeedRequest(String datafeedId) { + this.datafeedId = Objects.requireNonNull(datafeedId, "[datafeed_id] must not be null"); + } + + public String getDatafeedId() { + return datafeedId; + } + + public boolean isForce() { + return force; + } + + /** + * Used to forcefully delete a started datafeed. + * This method is quicker than stopping and deleting the datafeed. + * + * @param force When {@code true} forcefully delete a started datafeed. Defaults to {@code false} + */ + public void setForce(boolean force) { + this.force = force; + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + @Override + public int hashCode() { + return Objects.hash(datafeedId, force); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || obj.getClass() != getClass()) { + return false; + } + + DeleteDatafeedRequest other = (DeleteDatafeedRequest) obj; + return Objects.equals(datafeedId, other.datafeedId) && Objects.equals(force, other.force); + } + +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteJobResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteJobResponse.java deleted file mode 100644 index 86cafd9e093..00000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteJobResponse.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.elasticsearch.client.ml; - -import org.elasticsearch.action.support.master.AcknowledgedResponse; -import org.elasticsearch.common.xcontent.XContentParser; - -import java.io.IOException; -import java.util.Objects; - -/** - * Response acknowledging the Machine Learning Job request - */ -public class DeleteJobResponse extends AcknowledgedResponse { - - public DeleteJobResponse(boolean acknowledged) { - super(acknowledged); - } - - public DeleteJobResponse() { - } - - public static DeleteJobResponse fromXContent(XContentParser parser) throws IOException { - AcknowledgedResponse response = AcknowledgedResponse.fromXContent(parser); - return new DeleteJobResponse(response.isAcknowledged()); - } - - @Override - public boolean equals(Object other) { - if (this == other) { - return true; - } - - if (other == null || getClass() != other.getClass()) { - return false; - } - - DeleteJobResponse that = (DeleteJobResponse) other; - return isAcknowledged() == that.isAcknowledged(); - } - - @Override - public int hashCode() { - return Objects.hash(isAcknowledged()); - } - -} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java index 19db672e35b..547bc2e9a93 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java @@ -24,6 +24,7 @@ import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.elasticsearch.client.ml.CloseJobRequest; +import org.elasticsearch.client.ml.DeleteDatafeedRequest; import org.elasticsearch.client.ml.DeleteForecastRequest; import org.elasticsearch.client.ml.DeleteJobRequest; import org.elasticsearch.client.ml.FlushJobRequest; @@ -223,6 +224,20 @@ public class MLRequestConvertersTests extends ESTestCase { } } + public void testDeleteDatafeed() { + String datafeedId = randomAlphaOfLength(10); + DeleteDatafeedRequest deleteDatafeedRequest = new DeleteDatafeedRequest(datafeedId); + + Request request = MLRequestConverters.deleteDatafeed(deleteDatafeedRequest); + assertEquals(HttpDelete.METHOD_NAME, request.getMethod()); + assertEquals("/_xpack/ml/datafeeds/" + datafeedId, request.getEndpoint()); + assertEquals(Boolean.toString(false), request.getParameters().get("force")); + + deleteDatafeedRequest.setForce(true); + request = MLRequestConverters.deleteDatafeed(deleteDatafeedRequest); + assertEquals(Boolean.toString(true), request.getParameters().get("force")); + } + public void testDeleteForecast() throws Exception { String jobId = randomAlphaOfLength(10); DeleteForecastRequest deleteForecastRequest = new DeleteForecastRequest(jobId); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java index c0bf1055058..a07b4414843 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java @@ -25,9 +25,9 @@ import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.support.master.AcknowledgedResponse; import org.elasticsearch.client.ml.CloseJobRequest; import org.elasticsearch.client.ml.CloseJobResponse; +import org.elasticsearch.client.ml.DeleteDatafeedRequest; import org.elasticsearch.client.ml.DeleteForecastRequest; import org.elasticsearch.client.ml.DeleteJobRequest; -import org.elasticsearch.client.ml.DeleteJobResponse; import org.elasticsearch.client.ml.FlushJobRequest; import org.elasticsearch.client.ml.FlushJobResponse; import org.elasticsearch.client.ml.ForecastJobRequest; @@ -129,7 +129,7 @@ public class MachineLearningIT extends ESRestHighLevelClientTestCase { MachineLearningClient machineLearningClient = highLevelClient().machineLearning(); machineLearningClient.putJob(new PutJobRequest(job), RequestOptions.DEFAULT); - DeleteJobResponse response = execute(new DeleteJobRequest(jobId), + AcknowledgedResponse response = execute(new DeleteJobRequest(jobId), machineLearningClient::deleteJob, machineLearningClient::deleteJobAsync); @@ -312,6 +312,22 @@ public class MachineLearningIT extends ESRestHighLevelClientTestCase { assertThat(createdDatafeed.getIndices(), equalTo(datafeedConfig.getIndices())); } + public void testDeleteDatafeed() throws Exception { + String jobId = randomValidJobId(); + Job job = buildJob(jobId); + MachineLearningClient machineLearningClient = highLevelClient().machineLearning(); + machineLearningClient.putJob(new PutJobRequest(job), RequestOptions.DEFAULT); + + String datafeedId = "datafeed-" + jobId; + DatafeedConfig datafeedConfig = DatafeedConfig.builder(datafeedId, jobId).setIndices("some_data_index").build(); + execute(new PutDatafeedRequest(datafeedConfig), machineLearningClient::putDatafeed, machineLearningClient::putDatafeedAsync); + + AcknowledgedResponse response = execute(new DeleteDatafeedRequest(datafeedId), machineLearningClient::deleteDatafeed, + machineLearningClient::deleteDatafeedAsync); + + assertTrue(response.isAcknowledged()); + } + public void testDeleteForecast() throws Exception { String jobId = "test-delete-forecast"; diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java index 3e43792ac6a..09d32710eb1 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java @@ -34,9 +34,9 @@ import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.client.ml.CloseJobRequest; import org.elasticsearch.client.ml.CloseJobResponse; +import org.elasticsearch.client.ml.DeleteDatafeedRequest; import org.elasticsearch.client.ml.DeleteForecastRequest; import org.elasticsearch.client.ml.DeleteJobRequest; -import org.elasticsearch.client.ml.DeleteJobResponse; import org.elasticsearch.client.ml.FlushJobRequest; import org.elasticsearch.client.ml.FlushJobResponse; import org.elasticsearch.client.ml.ForecastJobRequest; @@ -264,7 +264,7 @@ public class MlClientDocumentationIT extends ESRestHighLevelClientTestCase { //tag::x-pack-delete-ml-job-request DeleteJobRequest deleteJobRequest = new DeleteJobRequest("my-first-machine-learning-job"); deleteJobRequest.setForce(false); //<1> - DeleteJobResponse deleteJobResponse = client.machineLearning().deleteJob(deleteJobRequest, RequestOptions.DEFAULT); + AcknowledgedResponse deleteJobResponse = client.machineLearning().deleteJob(deleteJobRequest, RequestOptions.DEFAULT); //end::x-pack-delete-ml-job-request //tag::x-pack-delete-ml-job-response @@ -273,9 +273,9 @@ public class MlClientDocumentationIT extends ESRestHighLevelClientTestCase { } { //tag::x-pack-delete-ml-job-request-listener - ActionListener listener = new ActionListener() { + ActionListener listener = new ActionListener() { @Override - public void onResponse(DeleteJobResponse deleteJobResponse) { + public void onResponse(AcknowledgedResponse acknowledgedResponse) { // <1> } @@ -587,6 +587,61 @@ public class MlClientDocumentationIT extends ESRestHighLevelClientTestCase { } } + public void testDeleteDatafeed() throws Exception { + RestHighLevelClient client = highLevelClient(); + + String jobId = "test-delete-datafeed-job"; + Job job = MachineLearningIT.buildJob(jobId); + client.machineLearning().putJob(new PutJobRequest(job), RequestOptions.DEFAULT); + + String datafeedId = "test-delete-datafeed"; + DatafeedConfig datafeed = DatafeedConfig.builder(datafeedId, jobId).setIndices("foo").build(); + client.machineLearning().putDatafeed(new PutDatafeedRequest(datafeed), RequestOptions.DEFAULT); + + { + //tag::x-pack-delete-ml-datafeed-request + DeleteDatafeedRequest deleteDatafeedRequest = new DeleteDatafeedRequest(datafeedId); + deleteDatafeedRequest.setForce(false); //<1> + AcknowledgedResponse deleteDatafeedResponse = client.machineLearning().deleteDatafeed( + deleteDatafeedRequest, RequestOptions.DEFAULT); + //end::x-pack-delete-ml-datafeed-request + + //tag::x-pack-delete-ml-datafeed-response + boolean isAcknowledged = deleteDatafeedResponse.isAcknowledged(); //<1> + //end::x-pack-delete-ml-datafeed-response + } + + // Recreate datafeed to allow second deletion + client.machineLearning().putDatafeed(new PutDatafeedRequest(datafeed), RequestOptions.DEFAULT); + + { + //tag::x-pack-delete-ml-datafeed-request-listener + ActionListener listener = new ActionListener() { + @Override + public void onResponse(AcknowledgedResponse acknowledgedResponse) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + //end::x-pack-delete-ml-datafeed-request-listener + + // Replace the empty listener by a blocking listener in test + final CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + + //tag::x-pack-delete-ml-datafeed-request-async + DeleteDatafeedRequest deleteDatafeedRequest = new DeleteDatafeedRequest(datafeedId); + client.machineLearning().deleteDatafeedAsync(deleteDatafeedRequest, RequestOptions.DEFAULT, listener); // <1> + //end::x-pack-delete-ml-datafeed-request-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + } + public void testGetBuckets() throws IOException, InterruptedException { RestHighLevelClient client = highLevelClient(); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteJobResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteDatafeedRequestTests.java similarity index 51% rename from client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteJobResponseTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteDatafeedRequestTests.java index 2eb4d51e191..e36aa855a7b 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteJobResponseTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteDatafeedRequestTests.java @@ -18,25 +18,25 @@ */ package org.elasticsearch.client.ml; -import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.test.AbstractXContentTestCase; +import org.elasticsearch.client.ml.datafeed.DatafeedConfigTests; +import org.elasticsearch.test.ESTestCase; -import java.io.IOException; +public class DeleteDatafeedRequestTests extends ESTestCase { -public class DeleteJobResponseTests extends AbstractXContentTestCase { - - @Override - protected DeleteJobResponse createTestInstance() { - return new DeleteJobResponse(); + public void testConstructor_GivenNullId() { + NullPointerException ex = expectThrows(NullPointerException.class, () -> new DeleteJobRequest(null)); + assertEquals("[job_id] must not be null", ex.getMessage()); } - @Override - protected DeleteJobResponse doParseInstance(XContentParser parser) throws IOException { - return DeleteJobResponse.fromXContent(parser); + public void testSetForce() { + DeleteDatafeedRequest deleteDatafeedRequest = createTestInstance(); + assertFalse(deleteDatafeedRequest.isForce()); + + deleteDatafeedRequest.setForce(true); + assertTrue(deleteDatafeedRequest.isForce()); } - @Override - protected boolean supportsUnknownFields() { - return false; + private DeleteDatafeedRequest createTestInstance() { + return new DeleteDatafeedRequest(DatafeedConfigTests.randomValidDatafeedId()); } } diff --git a/docs/java-rest/high-level/ml/delete-datafeed.asciidoc b/docs/java-rest/high-level/ml/delete-datafeed.asciidoc new file mode 100644 index 00000000000..68741651b33 --- /dev/null +++ b/docs/java-rest/high-level/ml/delete-datafeed.asciidoc @@ -0,0 +1,49 @@ +[[java-rest-high-x-pack-ml-delete-datafeed]] +=== Delete Datafeed API + +[[java-rest-high-x-pack-machine-learning-delete-datafeed-request]] +==== Delete Datafeed Request + +A `DeleteDatafeedRequest` object requires a non-null `datafeedId` and can optionally set `force`. +Can be executed as follows: + +["source","java",subs="attributes,callouts,macros"] +--------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-delete-ml-datafeed-request] +--------------------------------------------------- +<1> Use to forcefully delete a started datafeed; +this method is quicker than stopping and deleting the datafeed. +Defaults to `false`. + +[[java-rest-high-x-pack-machine-learning-delete-datafeed-response]] +==== Delete Datafeed Response + +The returned `AcknowledgedResponse` object indicates the acknowledgement of the request: +["source","java",subs="attributes,callouts,macros"] +--------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-delete-ml-datafeed-response] +--------------------------------------------------- +<1> `isAcknowledged` was the deletion request acknowledged or not + +[[java-rest-high-x-pack-machine-learning-delete-datafeed-async]] +==== Delete Datafeed Asynchronously + +This request can also be made asynchronously. +["source","java",subs="attributes,callouts,macros"] +--------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-delete-ml-datafeed-request-async] +--------------------------------------------------- +<1> The `DeleteDatafeedRequest` to execute and the `ActionListener` to alert on completion or error. + +The deletion request returns immediately. Once the request is completed, the `ActionListener` is +called back using the `onResponse` or `onFailure`. The latter indicates some failure occurred when +making the request. + +A typical listener for a `DeleteDatafeedRequest` could be defined as follows: + +["source","java",subs="attributes,callouts,macros"] +--------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-delete-ml-datafeed-request-listener] +--------------------------------------------------- +<1> The action to be taken when it is completed +<2> What to do when a failure occurs diff --git a/docs/java-rest/high-level/ml/delete-job.asciidoc b/docs/java-rest/high-level/ml/delete-job.asciidoc index 44a6a479409..43f1e2fb02b 100644 --- a/docs/java-rest/high-level/ml/delete-job.asciidoc +++ b/docs/java-rest/high-level/ml/delete-job.asciidoc @@ -18,7 +18,7 @@ Defaults to `false` [[java-rest-high-x-pack-machine-learning-delete-job-response]] ==== Delete Job Response -The returned `DeleteJobResponse` object indicates the acknowledgement of the request: +The returned `AcknowledgedResponse` object indicates the acknowledgement of the request: ["source","java",subs="attributes,callouts,macros"] --------------------------------------------------- include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-delete-ml-job-response] diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index 0be681a14d1..cb297d0f712 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -221,6 +221,7 @@ The Java High Level REST Client supports the following Machine Learning APIs: * <> * <> * <> +* <> * <> * <> * <> @@ -238,6 +239,7 @@ include::ml/close-job.asciidoc[] include::ml/update-job.asciidoc[] include::ml/flush-job.asciidoc[] include::ml/put-datafeed.asciidoc[] +include::ml/delete-datafeed.asciidoc[] include::ml/get-job-stats.asciidoc[] include::ml/forecast-job.asciidoc[] include::ml/delete-forecast.asciidoc[] From c3a817957d6ff02d0412c37d28c916457e2d9531 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 13 Sep 2018 10:42:26 -0700 Subject: [PATCH 58/78] [DOCS] Moves securing-communications to docs (#33640) --- .../configuring-tls-docker.asciidoc | 0 .../enabling-cipher-suites.asciidoc | 0 .../node-certificates.asciidoc | 0 .../securing-elasticsearch.asciidoc | 10 +++++----- .../separating-node-client-traffic.asciidoc | 4 ++-- .../setting-up-ssl.asciidoc | 0 .../securing-communications/tls-ad.asciidoc | 0 .../securing-communications/tls-http.asciidoc | 0 .../securing-communications/tls-ldap.asciidoc | 0 .../tls-transport.asciidoc | 0 x-pack/docs/en/security/configuring-es.asciidoc | 16 ++++++++++++---- .../en/security/securing-communications.asciidoc | 6 ++---- 12 files changed, 21 insertions(+), 15 deletions(-) rename {x-pack/docs/en => docs/reference}/security/securing-communications/configuring-tls-docker.asciidoc (100%) rename {x-pack/docs/en => docs/reference}/security/securing-communications/enabling-cipher-suites.asciidoc (100%) rename {x-pack/docs/en => docs/reference}/security/securing-communications/node-certificates.asciidoc (100%) rename {x-pack/docs/en => docs/reference}/security/securing-communications/securing-elasticsearch.asciidoc (84%) rename {x-pack/docs/en => docs/reference}/security/securing-communications/separating-node-client-traffic.asciidoc (94%) rename {x-pack/docs/en => docs/reference}/security/securing-communications/setting-up-ssl.asciidoc (100%) rename {x-pack/docs/en => docs/reference}/security/securing-communications/tls-ad.asciidoc (100%) rename {x-pack/docs/en => docs/reference}/security/securing-communications/tls-http.asciidoc (100%) rename {x-pack/docs/en => docs/reference}/security/securing-communications/tls-ldap.asciidoc (100%) rename {x-pack/docs/en => docs/reference}/security/securing-communications/tls-transport.asciidoc (100%) diff --git a/x-pack/docs/en/security/securing-communications/configuring-tls-docker.asciidoc b/docs/reference/security/securing-communications/configuring-tls-docker.asciidoc similarity index 100% rename from x-pack/docs/en/security/securing-communications/configuring-tls-docker.asciidoc rename to docs/reference/security/securing-communications/configuring-tls-docker.asciidoc diff --git a/x-pack/docs/en/security/securing-communications/enabling-cipher-suites.asciidoc b/docs/reference/security/securing-communications/enabling-cipher-suites.asciidoc similarity index 100% rename from x-pack/docs/en/security/securing-communications/enabling-cipher-suites.asciidoc rename to docs/reference/security/securing-communications/enabling-cipher-suites.asciidoc diff --git a/x-pack/docs/en/security/securing-communications/node-certificates.asciidoc b/docs/reference/security/securing-communications/node-certificates.asciidoc similarity index 100% rename from x-pack/docs/en/security/securing-communications/node-certificates.asciidoc rename to docs/reference/security/securing-communications/node-certificates.asciidoc diff --git a/x-pack/docs/en/security/securing-communications/securing-elasticsearch.asciidoc b/docs/reference/security/securing-communications/securing-elasticsearch.asciidoc similarity index 84% rename from x-pack/docs/en/security/securing-communications/securing-elasticsearch.asciidoc rename to docs/reference/security/securing-communications/securing-elasticsearch.asciidoc index 09cb118f684..6b919e065c6 100644 --- a/x-pack/docs/en/security/securing-communications/securing-elasticsearch.asciidoc +++ b/docs/reference/security/securing-communications/securing-elasticsearch.asciidoc @@ -29,17 +29,17 @@ information, see <>. For more information about encrypting communications across the Elastic Stack, see {xpack-ref}/encrypting-communications.html[Encrypting Communications]. -:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/securing-communications/node-certificates.asciidoc +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/security/securing-communications/node-certificates.asciidoc include::node-certificates.asciidoc[] -:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/securing-communications/tls-transport.asciidoc +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/security/securing-communications/tls-transport.asciidoc include::tls-transport.asciidoc[] -:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/securing-communications/tls-http.asciidoc +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/security/securing-communications/tls-http.asciidoc include::tls-http.asciidoc[] -:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/securing-communications/tls-ad.asciidoc +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/security/securing-communications/tls-ad.asciidoc include::tls-ad.asciidoc[] -:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/securing-communications/tls-ldap.asciidoc +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/security/securing-communications/tls-ldap.asciidoc include::tls-ldap.asciidoc[] \ No newline at end of file diff --git a/x-pack/docs/en/security/securing-communications/separating-node-client-traffic.asciidoc b/docs/reference/security/securing-communications/separating-node-client-traffic.asciidoc similarity index 94% rename from x-pack/docs/en/security/securing-communications/separating-node-client-traffic.asciidoc rename to docs/reference/security/securing-communications/separating-node-client-traffic.asciidoc index 887d4701d78..e911ad529c4 100644 --- a/x-pack/docs/en/security/securing-communications/separating-node-client-traffic.asciidoc +++ b/docs/reference/security/securing-communications/separating-node-client-traffic.asciidoc @@ -37,7 +37,7 @@ transport.profiles.client.bind_host: 1.1.1.1 <2> <2> The bind address for the network used for client communication If separate networks are not available, then -{xpack-ref}/ip-filtering.html[IP Filtering] can +{stack-ov}/ip-filtering.html[IP Filtering] can be enabled to limit access to the profiles. When using SSL for transport, a different set of certificates can also be used @@ -65,4 +65,4 @@ transport.profiles.client.xpack.security.ssl.client_authentication: none This setting keeps certificate authentication active for node-to-node traffic, but removes the requirement to distribute a signed certificate to transport clients. For more information, see -{xpack-ref}/java-clients.html#transport-client[Configuring the Transport Client to work with a Secured Cluster]. +{stack-ov}/java-clients.html#transport-client[Configuring the Transport Client to work with a Secured Cluster]. diff --git a/x-pack/docs/en/security/securing-communications/setting-up-ssl.asciidoc b/docs/reference/security/securing-communications/setting-up-ssl.asciidoc similarity index 100% rename from x-pack/docs/en/security/securing-communications/setting-up-ssl.asciidoc rename to docs/reference/security/securing-communications/setting-up-ssl.asciidoc diff --git a/x-pack/docs/en/security/securing-communications/tls-ad.asciidoc b/docs/reference/security/securing-communications/tls-ad.asciidoc similarity index 100% rename from x-pack/docs/en/security/securing-communications/tls-ad.asciidoc rename to docs/reference/security/securing-communications/tls-ad.asciidoc diff --git a/x-pack/docs/en/security/securing-communications/tls-http.asciidoc b/docs/reference/security/securing-communications/tls-http.asciidoc similarity index 100% rename from x-pack/docs/en/security/securing-communications/tls-http.asciidoc rename to docs/reference/security/securing-communications/tls-http.asciidoc diff --git a/x-pack/docs/en/security/securing-communications/tls-ldap.asciidoc b/docs/reference/security/securing-communications/tls-ldap.asciidoc similarity index 100% rename from x-pack/docs/en/security/securing-communications/tls-ldap.asciidoc rename to docs/reference/security/securing-communications/tls-ldap.asciidoc diff --git a/x-pack/docs/en/security/securing-communications/tls-transport.asciidoc b/docs/reference/security/securing-communications/tls-transport.asciidoc similarity index 100% rename from x-pack/docs/en/security/securing-communications/tls-transport.asciidoc rename to docs/reference/security/securing-communications/tls-transport.asciidoc diff --git a/x-pack/docs/en/security/configuring-es.asciidoc b/x-pack/docs/en/security/configuring-es.asciidoc index 5fd9ed610cb..7bdfbef08de 100644 --- a/x-pack/docs/en/security/configuring-es.asciidoc +++ b/x-pack/docs/en/security/configuring-es.asciidoc @@ -136,10 +136,15 @@ By default, events are logged to a dedicated `elasticsearch-access.log` file in easier analysis and control what events are logged. -- -include::securing-communications/securing-elasticsearch.asciidoc[] -include::securing-communications/configuring-tls-docker.asciidoc[] -include::securing-communications/enabling-cipher-suites.asciidoc[] -include::securing-communications/separating-node-client-traffic.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/security/securing-communications/securing-elasticsearch.asciidoc +include::{es-repo-dir}/security/securing-communications/securing-elasticsearch.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/security/securing-communications/configuring-tls-docker.asciidoc +include::{es-repo-dir}/security/securing-communications/configuring-tls-docker.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/security/securing-communications/enabling-cipher-suites.asciidoc +include::{es-repo-dir}/security/securing-communications/enabling-cipher-suites.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/security/securing-communications/separating-node-client-traffic.asciidoc +include::{es-repo-dir}/security/securing-communications/separating-node-client-traffic.asciidoc[] +:edit_url: include::authentication/configuring-active-directory-realm.asciidoc[] include::authentication/configuring-file-realm.asciidoc[] include::authentication/configuring-ldap-realm.asciidoc[] @@ -148,6 +153,9 @@ include::authentication/configuring-pki-realm.asciidoc[] include::authentication/configuring-saml-realm.asciidoc[] :edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/authentication/configuring-kerberos-realm.asciidoc include::authentication/configuring-kerberos-realm.asciidoc[] +:edit_url: include::fips-140-compliance.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/settings/security-settings.asciidoc include::{es-repo-dir}/settings/security-settings.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/settings/audit-settings.asciidoc include::{es-repo-dir}/settings/audit-settings.asciidoc[] diff --git a/x-pack/docs/en/security/securing-communications.asciidoc b/x-pack/docs/en/security/securing-communications.asciidoc index 11f6b3dc561..84f3b0bc27a 100644 --- a/x-pack/docs/en/security/securing-communications.asciidoc +++ b/x-pack/docs/en/security/securing-communications.asciidoc @@ -17,10 +17,8 @@ This section shows how to: The authentication of new nodes helps prevent a rogue node from joining the cluster and receiving data through replication. -:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/securing-communications/setting-up-ssl.asciidoc -include::securing-communications/setting-up-ssl.asciidoc[] - -//TO-DO: These sections can be removed when all links to them are removed. +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/security/securing-communications/setting-up-ssl.asciidoc +include::{es-repo-dir}/security/securing-communications/setting-up-ssl.asciidoc[] [[ciphers]] === Enabling cipher suites for stronger encryption From 7e51b960fbc109b916dfb321eb05b44358a2bd17 Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Thu, 13 Sep 2018 10:44:33 -0700 Subject: [PATCH 59/78] Adding index refresh (#33647) --- .../xpack/ml/action/TransportDeleteForecastAction.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteForecastAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteForecastAction.java index d80bbd1b342..e91f75964fc 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteForecastAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteForecastAction.java @@ -211,6 +211,7 @@ public class TransportDeleteForecastAction extends HandledTransportAction Date: Thu, 13 Sep 2018 13:55:08 -0400 Subject: [PATCH 60/78] Revert "Use serializable exception in GCP listeners (#33657)" This reverts commit 6dfe54c8381e2b9046de836fb07fabaa03a02452. --- .../index/shard/GlobalCheckpointListeners.java | 13 ++++++------- .../index/shard/GlobalCheckpointListenersTests.java | 9 ++++----- .../org/elasticsearch/index/shard/IndexShardIT.java | 4 ++-- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java b/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java index e738ebac160..224d5be17e1 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java +++ b/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java @@ -21,7 +21,6 @@ package org.elasticsearch.index.shard; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; -import org.elasticsearch.ElasticsearchTimeoutException; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.FutureUtils; @@ -34,6 +33,7 @@ import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import static org.elasticsearch.index.seqno.SequenceNumbers.NO_OPS_PERFORMED; import static org.elasticsearch.index.seqno.SequenceNumbers.UNASSIGNED_SEQ_NO; @@ -53,8 +53,7 @@ public class GlobalCheckpointListeners implements Closeable { * Callback when the global checkpoint is updated or the shard is closed. If the shard is closed, the value of the global checkpoint * will be set to {@link org.elasticsearch.index.seqno.SequenceNumbers#UNASSIGNED_SEQ_NO} and the exception will be non-null and an * instance of {@link IndexShardClosedException }. If the listener timed out waiting for notification then the exception will be - * non-null and an instance of {@link ElasticsearchTimeoutException}. If the global checkpoint is updated, the exception will be - * null. + * non-null and an instance of {@link TimeoutException}. If the global checkpoint is updated, the exception will be null. * * @param globalCheckpoint the updated global checkpoint * @param e if non-null, the shard is closed or the listener timed out @@ -97,8 +96,8 @@ public class GlobalCheckpointListeners implements Closeable { * shard is closed then the listener will be asynchronously notified on the executor used to construct this collection of global * checkpoint listeners. The listener will only be notified of at most one event, either the global checkpoint is updated or the shard * is closed. A listener must re-register after one of these events to receive subsequent events. Callers may add a timeout to be - * notified after if the timeout elapses. In this case, the listener will be notified with a {@link ElasticsearchTimeoutException}. - * Passing null for the timeout means no timeout will be associated to the listener. + * notified after if the timeout elapses. In this case, the listener will be notified with a {@link TimeoutException}. Passing null for + * the timeout means no timeout will be associated to the listener. * * @param currentGlobalCheckpoint the current global checkpoint known to the listener * @param listener the listener @@ -141,7 +140,7 @@ public class GlobalCheckpointListeners implements Closeable { removed = listeners != null && listeners.remove(listener) != null; } if (removed) { - final ElasticsearchTimeoutException e = new ElasticsearchTimeoutException(timeout.getStringRep()); + final TimeoutException e = new TimeoutException(timeout.getStringRep()); logger.trace("global checkpoint listener timed out", e); executor.execute(() -> notifyListener(listener, UNASSIGNED_SEQ_NO, e)); } @@ -226,7 +225,7 @@ public class GlobalCheckpointListeners implements Closeable { } else if (e instanceof IndexShardClosedException) { logger.warn("error notifying global checkpoint listener of closed shard", caught); } else { - assert e instanceof ElasticsearchTimeoutException : e; + assert e instanceof TimeoutException : e; logger.warn("error notifying global checkpoint listener of timeout", caught); } } diff --git a/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java b/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java index 4a6383c50d2..4ab278cc02a 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java @@ -21,7 +21,6 @@ package org.elasticsearch.index.shard; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; -import org.elasticsearch.ElasticsearchTimeoutException; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.EsExecutors; @@ -43,6 +42,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -515,11 +515,10 @@ public class GlobalCheckpointListenersTests extends ESTestCase { try { notified.set(true); assertThat(g, equalTo(UNASSIGNED_SEQ_NO)); - assertThat(e, instanceOf(ElasticsearchTimeoutException.class)); + assertThat(e, instanceOf(TimeoutException.class)); assertThat(e, hasToString(containsString(timeout.getStringRep()))); final ArgumentCaptor message = ArgumentCaptor.forClass(String.class); - final ArgumentCaptor t = - ArgumentCaptor.forClass(ElasticsearchTimeoutException.class); + final ArgumentCaptor t = ArgumentCaptor.forClass(TimeoutException.class); verify(mockLogger).trace(message.capture(), t.capture()); assertThat(message.getValue(), equalTo("global checkpoint listener timed out")); assertThat(t.getValue(), hasToString(containsString(timeout.getStringRep()))); @@ -551,7 +550,7 @@ public class GlobalCheckpointListenersTests extends ESTestCase { try { notified.set(true); assertThat(g, equalTo(UNASSIGNED_SEQ_NO)); - assertThat(e, instanceOf(ElasticsearchTimeoutException.class)); + assertThat(e, instanceOf(TimeoutException.class)); } finally { latch.countDown(); } diff --git a/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java b/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java index cd6a9b27b15..2c659ac60ec 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java +++ b/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java @@ -19,7 +19,6 @@ package org.elasticsearch.index.shard; import org.apache.lucene.store.LockObtainFailedException; -import org.elasticsearch.ElasticsearchTimeoutException; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; @@ -90,6 +89,7 @@ import java.util.Locale; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -798,7 +798,7 @@ public class IndexShardIT extends ESSingleNodeTestCase { notified.set(true); assertThat(g, equalTo(UNASSIGNED_SEQ_NO)); assertNotNull(e); - assertThat(e, instanceOf(ElasticsearchTimeoutException.class)); + assertThat(e, instanceOf(TimeoutException.class)); assertThat(e.getMessage(), equalTo(timeout.getStringRep())); } finally { latch.countDown(); From 040695b64e6452c9e54109b9d31fe3d1efdd69f7 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Thu, 13 Sep 2018 20:45:48 +0200 Subject: [PATCH 61/78] CORE: Disable Setting Type Validation (#33660) (#33669) * Reverts setting type validation introduced in #33503 --- .../main/java/org/elasticsearch/common/settings/Setting.java | 2 +- .../java/org/elasticsearch/common/settings/SettingTests.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/common/settings/Setting.java b/server/src/main/java/org/elasticsearch/common/settings/Setting.java index 5244cdd726d..23984e58749 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/Setting.java +++ b/server/src/main/java/org/elasticsearch/common/settings/Setting.java @@ -458,7 +458,7 @@ public class Setting implements ToXContentObject { * @return the raw string representation of the setting value */ String innerGetRaw(final Settings settings) { - return settings.get(getKey(), defaultValue.apply(settings), isListSetting()); + return settings.get(getKey(), defaultValue.apply(settings)); } /** Logs a deprecation warning if the setting is deprecated and used. */ diff --git a/server/src/test/java/org/elasticsearch/common/settings/SettingTests.java b/server/src/test/java/org/elasticsearch/common/settings/SettingTests.java index 30cfee81ddd..99fde0855f9 100644 --- a/server/src/test/java/org/elasticsearch/common/settings/SettingTests.java +++ b/server/src/test/java/org/elasticsearch/common/settings/SettingTests.java @@ -180,6 +180,7 @@ public class SettingTests extends ESTestCase { } } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/33135") public void testValidateStringSetting() { Settings settings = Settings.builder().putList("foo.bar", Arrays.asList("bla-a", "bla-b")).build(); Setting stringSetting = Setting.simpleString("foo.bar", Property.NodeScope); From b9d0c8f25cf6535e2f4583bc4cceaf44e5913501 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Thu, 13 Sep 2018 20:47:24 +0200 Subject: [PATCH 62/78] [CCR] Add monitoring mapping verification test (#33662) Added test that verifies that all fields in ShardFollowNodeTaskStatus are mapped in monitoring-es.json --- .../ccr/CcrStatsMonitoringDocTests.java | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsMonitoringDocTests.java b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsMonitoringDocTests.java index 70b73e5eed0..ed893410c88 100644 --- a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsMonitoringDocTests.java +++ b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsMonitoringDocTests.java @@ -7,12 +7,16 @@ package org.elasticsearch.xpack.monitoring.collector.ccr; import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.common.xcontent.support.XContentMapValues; import org.elasticsearch.xpack.core.ccr.ShardFollowNodeTaskStatus; import org.elasticsearch.xpack.core.monitoring.MonitoredSystem; import org.elasticsearch.xpack.core.monitoring.exporter.MonitoringDoc; +import org.elasticsearch.xpack.core.monitoring.exporter.MonitoringTemplateUtils; import org.elasticsearch.xpack.monitoring.exporter.BaseMonitoringDocTestCase; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; @@ -20,13 +24,17 @@ import org.junit.Before; import java.io.IOException; import java.util.Collections; +import java.util.Map; import java.util.NavigableMap; import java.util.TreeMap; +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasToString; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.mockito.Mockito.mock; @@ -174,4 +182,65 @@ public class CcrStatsMonitoringDocTests extends BaseMonitoringDocTestCase fetchExceptions = + new TreeMap<>(Collections.singletonMap(1L, new ElasticsearchException("shard is sad"))); + final ShardFollowNodeTaskStatus status = new ShardFollowNodeTaskStatus( + "cluster_alias:leader_index", + "follower_index", + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 100, + 10, + 0, + 10, + 100, + 10, + 10, + 0, + 10, + fetchExceptions, + 2); + XContentBuilder builder = jsonBuilder(); + builder.value(status); + Map serializedStatus = XContentHelper.convertToMap(XContentType.JSON.xContent(), Strings.toString(builder), false); + + Map template = + XContentHelper.convertToMap(XContentType.JSON.xContent(), MonitoringTemplateUtils.loadTemplate("es"), false); + Map ccrStatsMapping = (Map) XContentMapValues.extractValue("mappings.doc.properties.ccr_stats.properties", template); + + assertThat(serializedStatus.size(), equalTo(ccrStatsMapping.size())); + for (Map.Entry entry : serializedStatus.entrySet()) { + String fieldName = entry.getKey(); + Map fieldMapping = (Map) ccrStatsMapping.get(fieldName); + assertThat(fieldMapping, notNullValue()); + + Object fieldValue = entry.getValue(); + String fieldType = (String) fieldMapping.get("type"); + if (fieldValue instanceof Long || fieldValue instanceof Integer) { + assertThat("expected long field type for field [" + fieldName + "]", fieldType, + anyOf(equalTo("long"), equalTo("integer"))); + } else if (fieldValue instanceof String) { + assertThat("expected keyword field type for field [" + fieldName + "]", fieldType, + anyOf(equalTo("keyword"), equalTo("text"))); + } else { + // Manual test specific object fields and if not just fail: + if (fieldName.equals("fetch_exceptions")) { + assertThat(XContentMapValues.extractValue("properties.from_seq_no.type", fieldMapping), equalTo("long")); + assertThat(XContentMapValues.extractValue("properties.exception.type", fieldMapping), equalTo("text")); + } else { + fail("unexpected field value type [" + fieldValue.getClass() + "] for field [" + fieldName + "]"); + } + } + } + } + } From 53ba253aa4602158881814609872435e6d66feeb Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Thu, 13 Sep 2018 20:52:00 +0200 Subject: [PATCH 63/78] [CCR] Add validation for max_retry_delay (#33648) --- .../ccr/action/FollowIndexRequestTests.java | 22 ++++++++++ .../xpack/core/ccr/AutoFollowMetadata.java | 2 +- .../core/ccr/action/FollowIndexAction.java | 32 +++++++++++---- .../action/PutAutoFollowPatternAction.java | 19 ++++++++- .../PutAutoFollowPatternRequestTests.java | 41 +++++++++++++++++++ 5 files changed, 105 insertions(+), 11 deletions(-) diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/FollowIndexRequestTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/FollowIndexRequestTests.java index 2017fa2fdb9..e5f7e693a7f 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/FollowIndexRequestTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/FollowIndexRequestTests.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.ccr.action; +import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractStreamableXContentTestCase; @@ -12,6 +13,10 @@ import org.elasticsearch.xpack.core.ccr.action.FollowIndexAction; import java.io.IOException; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + public class FollowIndexRequestTests extends AbstractStreamableXContentTestCase { @Override @@ -39,4 +44,21 @@ public class FollowIndexRequestTests extends AbstractStreamableXContentTestCase< randomIntBetween(1, Integer.MAX_VALUE), randomNonNegativeLong(), randomIntBetween(1, Integer.MAX_VALUE), randomIntBetween(1, Integer.MAX_VALUE), TimeValue.timeValueMillis(500), TimeValue.timeValueMillis(500)); } + + public void testValidate() { + FollowIndexAction.Request request = new FollowIndexAction.Request("index1", "index2", null, null, null, null, + null, TimeValue.ZERO, null); + ActionRequestValidationException validationException = request.validate(); + assertThat(validationException, notNullValue()); + assertThat(validationException.getMessage(), containsString("[max_retry_delay] must be positive but was [0ms]")); + + request = new FollowIndexAction.Request("index1", "index2", null, null, null, null, null, TimeValue.timeValueMinutes(10), null); + validationException = request.validate(); + assertThat(validationException, notNullValue()); + assertThat(validationException.getMessage(), containsString("[max_retry_delay] must be less than [5m] but was [10m]")); + + request = new FollowIndexAction.Request("index1", "index2", null, null, null, null, null, TimeValue.timeValueMinutes(1), null); + validationException = request.validate(); + assertThat(validationException, nullValue()); + } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/AutoFollowMetadata.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/AutoFollowMetadata.java index 71fd13d0b50..9c64ea3da76 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/AutoFollowMetadata.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/AutoFollowMetadata.java @@ -169,7 +169,7 @@ public class AutoFollowMetadata extends AbstractNamedDiffable i public static final ParseField MAX_BATCH_SIZE_IN_BYTES = new ParseField("max_batch_size_in_bytes"); public static final ParseField MAX_CONCURRENT_WRITE_BATCHES = new ParseField("max_concurrent_write_batches"); public static final ParseField MAX_WRITE_BUFFER_SIZE = new ParseField("max_write_buffer_size"); - public static final ParseField MAX_RETRY_DELAY = new ParseField("retry_timeout"); + public static final ParseField MAX_RETRY_DELAY = new ParseField("max_retry_delay"); public static final ParseField IDLE_SHARD_RETRY_DELAY = new ParseField("idle_shard_retry_delay"); @SuppressWarnings("unchecked") diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/FollowIndexAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/FollowIndexAction.java index 2c311356d49..d5a0b0408c5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/FollowIndexAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/FollowIndexAction.java @@ -23,6 +23,8 @@ import org.elasticsearch.common.xcontent.XContentParser; import java.io.IOException; import java.util.Objects; +import static org.elasticsearch.action.ValidateActions.addValidationError; + public final class FollowIndexAction extends Action { public static final FollowIndexAction INSTANCE = new FollowIndexAction(); @@ -33,8 +35,9 @@ public final class FollowIndexAction extends Action { public static final int DEFAULT_MAX_CONCURRENT_READ_BATCHES = 1; public static final int DEFAULT_MAX_CONCURRENT_WRITE_BATCHES = 1; public static final long DEFAULT_MAX_BATCH_SIZE_IN_BYTES = Long.MAX_VALUE; - public static final TimeValue DEFAULT_RETRY_TIMEOUT = new TimeValue(500); - public static final TimeValue DEFAULT_IDLE_SHARD_RETRY_DELAY = TimeValue.timeValueSeconds(10); + static final TimeValue DEFAULT_MAX_RETRY_DELAY = new TimeValue(500); + static final TimeValue DEFAULT_IDLE_SHARD_RETRY_DELAY = TimeValue.timeValueSeconds(10); + static final TimeValue MAX_RETRY_DELAY = TimeValue.timeValueMinutes(5); private FollowIndexAction() { super(NAME); @@ -54,7 +57,7 @@ public final class FollowIndexAction extends Action { private static final ParseField MAX_BATCH_SIZE_IN_BYTES = new ParseField("max_batch_size_in_bytes"); private static final ParseField MAX_CONCURRENT_WRITE_BATCHES = new ParseField("max_concurrent_write_batches"); private static final ParseField MAX_WRITE_BUFFER_SIZE = new ParseField("max_write_buffer_size"); - private static final ParseField MAX_RETRY_DELAY = new ParseField("max_retry_delay"); + private static final ParseField MAX_RETRY_DELAY_FIELD = new ParseField("max_retry_delay"); private static final ParseField IDLE_SHARD_RETRY_DELAY = new ParseField("idle_shard_retry_delay"); private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, true, (args, followerIndex) -> { @@ -75,8 +78,8 @@ public final class FollowIndexAction extends Action { PARSER.declareInt(ConstructingObjectParser.optionalConstructorArg(), MAX_WRITE_BUFFER_SIZE); PARSER.declareField( ConstructingObjectParser.optionalConstructorArg(), - (p, c) -> TimeValue.parseTimeValue(p.text(), MAX_RETRY_DELAY.getPreferredName()), - MAX_RETRY_DELAY, + (p, c) -> TimeValue.parseTimeValue(p.text(), MAX_RETRY_DELAY_FIELD.getPreferredName()), + MAX_RETRY_DELAY_FIELD, ObjectParser.ValueType.STRING); PARSER.declareField( ConstructingObjectParser.optionalConstructorArg(), @@ -202,7 +205,7 @@ public final class FollowIndexAction extends Action { throw new IllegalArgumentException(MAX_WRITE_BUFFER_SIZE.getPreferredName() + " must be larger than 0"); } - final TimeValue actualRetryTimeout = maxRetryDelay == null ? DEFAULT_RETRY_TIMEOUT : maxRetryDelay; + final TimeValue actualRetryTimeout = maxRetryDelay == null ? DEFAULT_MAX_RETRY_DELAY : maxRetryDelay; final TimeValue actualIdleShardRetryDelay = idleShardRetryDelay == null ? DEFAULT_IDLE_SHARD_RETRY_DELAY : idleShardRetryDelay; this.leaderIndex = leaderIndex; @@ -222,7 +225,20 @@ public final class FollowIndexAction extends Action { @Override public ActionRequestValidationException validate() { - return null; + ActionRequestValidationException validationException = null; + + if (maxRetryDelay.millis() <= 0) { + String message = "[" + MAX_RETRY_DELAY_FIELD.getPreferredName() + "] must be positive but was [" + + maxRetryDelay.getStringRep() + "]"; + validationException = addValidationError(message, validationException); + } + if (maxRetryDelay.millis() > FollowIndexAction.MAX_RETRY_DELAY.millis()) { + String message = "[" + MAX_RETRY_DELAY_FIELD.getPreferredName() + "] must be less than [" + MAX_RETRY_DELAY + + "] but was [" + maxRetryDelay.getStringRep() + "]"; + validationException = addValidationError(message, validationException); + } + + return validationException; } @Override @@ -264,7 +280,7 @@ public final class FollowIndexAction extends Action { builder.field(MAX_WRITE_BUFFER_SIZE.getPreferredName(), maxWriteBufferSize); builder.field(MAX_CONCURRENT_READ_BATCHES.getPreferredName(), maxConcurrentReadBatches); builder.field(MAX_CONCURRENT_WRITE_BATCHES.getPreferredName(), maxConcurrentWriteBatches); - builder.field(MAX_RETRY_DELAY.getPreferredName(), maxRetryDelay.getStringRep()); + builder.field(MAX_RETRY_DELAY_FIELD.getPreferredName(), maxRetryDelay.getStringRep()); builder.field(IDLE_SHARD_RETRY_DELAY.getPreferredName(), idleShardRetryDelay.getStringRep()); } builder.endObject(); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/PutAutoFollowPatternAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/PutAutoFollowPatternAction.java index dc69795bb4a..01ebd3f1d81 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/PutAutoFollowPatternAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/PutAutoFollowPatternAction.java @@ -94,10 +94,25 @@ public class PutAutoFollowPatternAction extends Action { public ActionRequestValidationException validate() { ActionRequestValidationException validationException = null; if (leaderClusterAlias == null) { - validationException = addValidationError("leaderClusterAlias is missing", validationException); + validationException = addValidationError("[" + LEADER_CLUSTER_ALIAS_FIELD.getPreferredName() + + "] is missing", validationException); } if (leaderIndexPatterns == null || leaderIndexPatterns.isEmpty()) { - validationException = addValidationError("leaderIndexPatterns is missing", validationException); + validationException = addValidationError("[" + LEADER_INDEX_PATTERNS_FIELD.getPreferredName() + + "] is missing", validationException); + } + if (maxRetryDelay != null) { + if (maxRetryDelay.millis() <= 0) { + String message = "[" + AutoFollowPattern.MAX_RETRY_DELAY.getPreferredName() + "] must be positive but was [" + + maxRetryDelay.getStringRep() + "]"; + validationException = addValidationError(message, validationException); + } + if (maxRetryDelay.millis() > FollowIndexAction.MAX_RETRY_DELAY.millis()) { + String message = "[" + AutoFollowPattern.MAX_RETRY_DELAY.getPreferredName() + "] must be less than [" + + FollowIndexAction.MAX_RETRY_DELAY + + "] but was [" + maxRetryDelay.getStringRep() + "]"; + validationException = addValidationError(message, validationException); + } } return validationException; } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ccr/action/PutAutoFollowPatternRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ccr/action/PutAutoFollowPatternRequestTests.java index f11e1885e80..ced49bbae12 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ccr/action/PutAutoFollowPatternRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ccr/action/PutAutoFollowPatternRequestTests.java @@ -5,12 +5,18 @@ */ package org.elasticsearch.xpack.core.ccr.action; +import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractStreamableXContentTestCase; import java.io.IOException; import java.util.Arrays; +import java.util.Collections; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; public class PutAutoFollowPatternRequestTests extends AbstractStreamableXContentTestCase { @@ -60,4 +66,39 @@ public class PutAutoFollowPatternRequestTests extends AbstractStreamableXContent } return request; } + + public void testValidate() { + PutAutoFollowPatternAction.Request request = new PutAutoFollowPatternAction.Request(); + ActionRequestValidationException validationException = request.validate(); + assertThat(validationException, notNullValue()); + assertThat(validationException.getMessage(), containsString("[leader_cluster_alias] is missing")); + + request.setLeaderClusterAlias("_alias"); + validationException = request.validate(); + assertThat(validationException, notNullValue()); + assertThat(validationException.getMessage(), containsString("[leader_index_patterns] is missing")); + + request.setLeaderIndexPatterns(Collections.emptyList()); + validationException = request.validate(); + assertThat(validationException, notNullValue()); + assertThat(validationException.getMessage(), containsString("[leader_index_patterns] is missing")); + + request.setLeaderIndexPatterns(Collections.singletonList("logs-*")); + validationException = request.validate(); + assertThat(validationException, nullValue()); + + request.setMaxRetryDelay(TimeValue.ZERO); + validationException = request.validate(); + assertThat(validationException, notNullValue()); + assertThat(validationException.getMessage(), containsString("[max_retry_delay] must be positive but was [0ms]")); + + request.setMaxRetryDelay(TimeValue.timeValueMinutes(10)); + validationException = request.validate(); + assertThat(validationException, notNullValue()); + assertThat(validationException.getMessage(), containsString("[max_retry_delay] must be less than [5m] but was [10m]")); + + request.setMaxRetryDelay(TimeValue.timeValueMinutes(1)); + validationException = request.validate(); + assertThat(validationException, nullValue()); + } } From 60ab4f97ab15c138412e2af10deba57a6b4e5e9d Mon Sep 17 00:00:00 2001 From: Costin Leau Date: Thu, 13 Sep 2018 21:55:44 +0300 Subject: [PATCH 64/78] SQL: Return correct catalog separator in JDBC (#33670) JDBC DatabaseMetadata returns correct separator (:) for catalog/cluster names. Fix #33654 --- .../sql/jdbc/jdbc/JdbcDatabaseMetaData.java | 2 +- .../jdbc/jdbc/JdbcDatabaseMetaDataTests.java | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugin/sql/jdbc/src/test/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcDatabaseMetaDataTests.java diff --git a/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcDatabaseMetaData.java b/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcDatabaseMetaData.java index 5cb63a33763..d2e24f3edac 100644 --- a/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcDatabaseMetaData.java +++ b/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcDatabaseMetaData.java @@ -368,7 +368,7 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper { @Override public String getCatalogSeparator() throws SQLException { - return "."; + return ":"; } @Override diff --git a/x-pack/plugin/sql/jdbc/src/test/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcDatabaseMetaDataTests.java b/x-pack/plugin/sql/jdbc/src/test/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcDatabaseMetaDataTests.java new file mode 100644 index 00000000000..cfa6e797260 --- /dev/null +++ b/x-pack/plugin/sql/jdbc/src/test/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcDatabaseMetaDataTests.java @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.sql.jdbc.jdbc; + +import org.elasticsearch.test.ESTestCase; + +public class JdbcDatabaseMetaDataTests extends ESTestCase { + + private JdbcDatabaseMetaData md = new JdbcDatabaseMetaData(null); + + public void testSeparators() throws Exception { + assertEquals(":", md.getCatalogSeparator()); + assertEquals("\"", md.getIdentifierQuoteString()); + assertEquals("\\", md.getSearchStringEscape()); + + } +} From 32a22ca00e5329b785e4aa248677aab7d3349191 Mon Sep 17 00:00:00 2001 From: Costin Leau Date: Thu, 13 Sep 2018 22:07:23 +0300 Subject: [PATCH 65/78] DOC: improved wording in SQL client app section --- docs/reference/sql/endpoints/client-apps/index.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/sql/endpoints/client-apps/index.asciidoc b/docs/reference/sql/endpoints/client-apps/index.asciidoc index ee9891040d0..2c497518328 100644 --- a/docs/reference/sql/endpoints/client-apps/index.asciidoc +++ b/docs/reference/sql/endpoints/client-apps/index.asciidoc @@ -3,7 +3,7 @@ [[sql-client-apps]] == SQL Client Applications -Thanks to its <> interface, {es-sql} supports a broad range of applications. +Thanks to its <> interface, a broad range of third-party applications can use {es}'s SQL capabilities. This section lists, in alphabetical order, a number of them and their respective configuration - the list however is by no means comprehensive (feel free to https://www.elastic.co/blog/art-of-pull-request[submit a PR] to improve it): as long as the app can use the {es-sql} driver, it can use {es-sql}. From 7dd22f09dcebb44da6e9f6aa22bdd2f7ceb78c9a Mon Sep 17 00:00:00 2001 From: Michael Basnight Date: Thu, 13 Sep 2018 15:17:37 -0500 Subject: [PATCH 66/78] Mute failing JdbcSqlSpec functions Relates #33687 --- .../sql/src/main/resources/string-functions.sql-spec | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/x-pack/qa/sql/src/main/resources/string-functions.sql-spec b/x-pack/qa/sql/src/main/resources/string-functions.sql-spec index c0b0430b278..8fe35780443 100644 --- a/x-pack/qa/sql/src/main/resources/string-functions.sql-spec +++ b/x-pack/qa/sql/src/main/resources/string-functions.sql-spec @@ -157,14 +157,16 @@ SELECT SUBSTRING('Elasticsearch', 10, 10) sub; ucaseFilter SELECT UCASE(gender) uppercased, COUNT(*) count FROM "test_emp" WHERE UCASE(gender) = 'F' GROUP BY UCASE(gender); -ucaseInline1 -SELECT UCASE('ElAsTiC') upper; +//https://github.com/elastic/elasticsearch/issues/33687 +//ucaseInline1 +//SELECT UCASE('ElAsTiC') upper; ucaseInline2 SELECT UCASE('') upper; -ucaseInline3 -SELECT UCASE(' elastic ') upper; +//https://github.com/elastic/elasticsearch/issues/33687 +//ucaseInline3 +//SELECT UCASE(' elastic ') upper; // // Group and order by From 3914a980f799b39a0c2983fba9640ae50f8d2e4c Mon Sep 17 00:00:00 2001 From: Jay Modi Date: Thu, 13 Sep 2018 14:40:36 -0600 Subject: [PATCH 67/78] Security: remove wrapping in put user response (#33512) This change removes the wrapping of the created field in the put user response. The created field was added as a top level field in #32332, while also still being wrapped within the `user` object of the response. Since the value is available in both formats in 6.x, we can remove the wrapped version for 7.0. --- docs/reference/migration/migrate_7_0/api.asciidoc | 6 ++++++ x-pack/docs/en/rest-api/security/create-users.asciidoc | 3 --- .../xpack/core/security/action/user/PutUserResponse.java | 8 +++++--- .../security/rest/action/user/RestPutUserAction.java | 7 +------ .../resources/rest-api-spec/test/roles/11_idx_arrays.yml | 2 +- .../test/resources/rest-api-spec/test/users/10_basic.yml | 4 ++-- .../rest-api-spec/test/users/15_overwrite_user.yml | 2 +- .../resources/rest-api-spec/test/users/16_update_user.yml | 4 ++-- .../rest-api-spec/test/remote_cluster/10_basic.yml | 2 +- .../rest-api-spec/test/old_cluster/20_security.yml | 2 +- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/reference/migration/migrate_7_0/api.asciidoc b/docs/reference/migration/migrate_7_0/api.asciidoc index ce2d817ac50..a58223023bd 100644 --- a/docs/reference/migration/migrate_7_0/api.asciidoc +++ b/docs/reference/migration/migrate_7_0/api.asciidoc @@ -87,3 +87,9 @@ depending on whether {security} is enabled. Previously a 404 - NOT FOUND (IndexNotFoundException) could be returned in case the current user was not authorized for any alias. An empty response with status 200 - OK is now returned instead at all times. + +==== Put User API response no longer has `user` object + +The Put User API response was changed in 6.5.0 to add the `created` field +outside of the user object where it previously had been. In 7.0.0 the user +object has been removed in favor of the top level `created` field. diff --git a/x-pack/docs/en/rest-api/security/create-users.asciidoc b/x-pack/docs/en/rest-api/security/create-users.asciidoc index 789e8c7e80d..d18618af273 100644 --- a/x-pack/docs/en/rest-api/security/create-users.asciidoc +++ b/x-pack/docs/en/rest-api/security/create-users.asciidoc @@ -90,9 +90,6 @@ created or updated. [source,js] -------------------------------------------------- { - "user": { - "created" : true - }, "created": true <1> } -------------------------------------------------- diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserResponse.java index 4d0e5fdfa4b..c8958251e16 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserResponse.java @@ -9,7 +9,7 @@ package org.elasticsearch.xpack.core.security.action.user; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.common.xcontent.ToXContentFragment; +import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import java.io.IOException; @@ -18,7 +18,7 @@ import java.io.IOException; * Response when adding a user to the security index. Returns a * single boolean field for whether the user was created or updated. */ -public class PutUserResponse extends ActionResponse implements ToXContentFragment { +public class PutUserResponse extends ActionResponse implements ToXContentObject { private boolean created; @@ -47,6 +47,8 @@ public class PutUserResponse extends ActionResponse implements ToXContentFragmen @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - return builder.field("created", created); + return builder.startObject() + .field("created", created) + .endObject(); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestPutUserAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestPutUserAction.java index d6fc6aae381..521ab76c96b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestPutUserAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestPutUserAction.java @@ -58,13 +58,8 @@ public class RestPutUserAction extends SecurityBaseRestHandler implements RestRe return channel -> requestBuilder.execute(new RestBuilderListener(channel) { @Override public RestResponse buildResponse(PutUserResponse putUserResponse, XContentBuilder builder) throws Exception { - builder.startObject() - .startObject("user"); // TODO in 7.0 remove wrapping of response in the user object and just return the object putUserResponse.toXContent(builder, request); - builder.endObject(); - - putUserResponse.toXContent(builder, request); - return new BytesRestResponse(RestStatus.OK, builder.endObject()); + return new BytesRestResponse(RestStatus.OK, builder); } }); } diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/roles/11_idx_arrays.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/roles/11_idx_arrays.yml index 4d32866ee8b..84e2ae4d412 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/roles/11_idx_arrays.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/roles/11_idx_arrays.yml @@ -51,7 +51,7 @@ teardown: "password": "s3krit", "roles" : [ "admin_role2" ] } - - match: { user: { created: true } } + - match: { created: true } - do: index: diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/users/10_basic.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/users/10_basic.yml index 5e5138c88fb..ea152bd677c 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/users/10_basic.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/users/10_basic.yml @@ -30,7 +30,7 @@ teardown: "key2" : "val2" } } - - match: { user: { created: true } } + - match: { created: true } - do: headers: @@ -65,7 +65,7 @@ teardown: "key2" : "val2" } } - - match: { user: { created: true } } + - match: { created: true } - do: headers: diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/users/15_overwrite_user.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/users/15_overwrite_user.yml index 66bcc9d1c5a..efe4d4e4c92 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/users/15_overwrite_user.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/users/15_overwrite_user.yml @@ -51,7 +51,7 @@ teardown: "key2" : "val2" } } - - match: { user: { created: false } } + - match: { created: false } - do: xpack.security.get_user: diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/users/16_update_user.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/users/16_update_user.yml index abe6f44369a..2a477e8bfbb 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/users/16_update_user.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/users/16_update_user.yml @@ -66,7 +66,7 @@ teardown: "key2" : "val2" } } - - match: { user: { created: false } } + - match: { created: false } # validate existing password works - do: @@ -103,7 +103,7 @@ teardown: "key3" : "val3" } } - - match: { user: { created: false } } + - match: { created: false } # validate old password doesn't work - do: diff --git a/x-pack/qa/multi-cluster-search-security/src/test/resources/rest-api-spec/test/remote_cluster/10_basic.yml b/x-pack/qa/multi-cluster-search-security/src/test/resources/rest-api-spec/test/remote_cluster/10_basic.yml index 6fa2b1e31a1..adcf0cf0770 100644 --- a/x-pack/qa/multi-cluster-search-security/src/test/resources/rest-api-spec/test/remote_cluster/10_basic.yml +++ b/x-pack/qa/multi-cluster-search-security/src/test/resources/rest-api-spec/test/remote_cluster/10_basic.yml @@ -195,4 +195,4 @@ setup: "password": "s3krit", "roles" : [ ] } - - match: { user: { created: false } } + - match: { created: false } diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/20_security.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/20_security.yml index 119f6f48749..c145d439424 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/20_security.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/20_security.yml @@ -9,7 +9,7 @@ "password" : "x-pack-test-password", "roles" : [ "native_role" ] } - - match: { user: { created: true } } + - match: { created: true } - do: xpack.security.put_role: From d3e27ff2f62371cd93d14f61cd8a1bd881129f99 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad <902768+bizybot@users.noreply.github.com> Date: Fri, 14 Sep 2018 10:07:19 +1000 Subject: [PATCH 68/78] [Kerberos] Move tests based on SimpleKdc to evil-tests (#33492) We have a test dependency on Apache Mina when using SimpleKdcServer for testing Kerberos. When checking for LDAP backend connectivity, the code checks for deadlocks which require additional security permissions accessClassInPackage.sun.reflect. As this is only for test and we do not want to add security permissions to production, this commit moves these tests and related classes to x-pack evil-tests where they can run with security manager disabled. The plan is to handle the security manager exception in the upstream issue DIRMINA-1093 and then once the release is available to run these tests with security manager enabled. Closes #32739 --- .../kerberos/KerberosRealmCacheTests.java | 4 +- .../kerberos/KerberosRealmSettingsTests.java | 4 +- .../authc/kerberos/KerberosRealmTestCase.java | 52 ++++++++++++++++- .../authc/kerberos/KerberosRealmTests.java | 2 +- x-pack/qa/evil-tests/build.gradle | 4 +- .../authc/kerberos/KerberosTestCase.java | 56 ++----------------- .../KerberosTicketValidatorTests.java | 4 +- .../authc/kerberos/SimpleKdcLdapServer.java | 0 .../kerberos/SimpleKdcLdapServerTests.java | 0 .../security/authc/kerberos/SpnegoClient.java | 0 .../evil-tests}/src/test/resources/kdc.ldiff | 0 11 files changed, 65 insertions(+), 61 deletions(-) rename x-pack/{plugin/security => qa/evil-tests}/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTestCase.java (73%) rename x-pack/{plugin/security => qa/evil-tests}/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTicketValidatorTests.java (96%) rename x-pack/{plugin/security => qa/evil-tests}/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServer.java (100%) rename x-pack/{plugin/security => qa/evil-tests}/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServerTests.java (100%) rename x-pack/{plugin/security => qa/evil-tests}/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SpnegoClient.java (100%) rename x-pack/{plugin/security => qa/evil-tests}/src/test/resources/kdc.ldiff (100%) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java index 69ebe15c5d7..ee2e2675e18 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java @@ -102,8 +102,8 @@ public class KerberosRealmCacheTests extends KerberosRealmTestCase { public void testAuthenticateWithValidTicketSucessAuthnWithUserDetailsWhenCacheDisabled() throws LoginException, GSSException, IOException { // if cache.ttl <= 0 then the cache is disabled - settings = KerberosTestCase.buildKerberosRealmSettings( - KerberosTestCase.writeKeyTab(dir.resolve("key.keytab"), randomAlphaOfLength(4)).toString(), 100, "0m", true, + settings = buildKerberosRealmSettings( + writeKeyTab(dir.resolve("key.keytab"), randomAlphaOfLength(4)).toString(), 100, "0m", true, randomBoolean()); final String username = randomPrincipalName(); final String outToken = randomAlphaOfLength(10); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmSettingsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmSettingsTests.java index 2e47d03d49d..55687d51888 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmSettingsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmSettingsTests.java @@ -27,12 +27,12 @@ public class KerberosRealmSettingsTests extends ESTestCase { configDir = Files.createDirectory(configDir); } final String keytabPathConfig = "config" + dir.getFileSystem().getSeparator() + "http.keytab"; - KerberosTestCase.writeKeyTab(dir.resolve(keytabPathConfig), null); + KerberosRealmTestCase.writeKeyTab(dir.resolve(keytabPathConfig), null); final Integer maxUsers = randomInt(); final String cacheTTL = randomLongBetween(10L, 100L) + "m"; final boolean enableDebugLogs = randomBoolean(); final boolean removeRealmName = randomBoolean(); - final Settings settings = KerberosTestCase.buildKerberosRealmSettings(keytabPathConfig, maxUsers, cacheTTL, enableDebugLogs, + final Settings settings = KerberosRealmTestCase.buildKerberosRealmSettings(keytabPathConfig, maxUsers, cacheTTL, enableDebugLogs, removeRealmName); assertThat(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(settings), equalTo(keytabPathConfig)); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java index dd83da49a0b..8f959a26bb8 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.security.authc.kerberos; import org.elasticsearch.action.ActionListener; import org.elasticsearch.client.Client; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; @@ -30,6 +31,10 @@ import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.junit.After; import org.junit.Before; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.Collections; @@ -71,7 +76,7 @@ public abstract class KerberosRealmTestCase extends ESTestCase { resourceWatcherService = new ResourceWatcherService(Settings.EMPTY, threadPool); dir = createTempDir(); globalSettings = Settings.builder().put("path.home", dir).build(); - settings = KerberosTestCase.buildKerberosRealmSettings(KerberosTestCase.writeKeyTab(dir.resolve("key.keytab"), "asa").toString(), + settings = buildKerberosRealmSettings(writeKeyTab(dir.resolve("key.keytab"), "asa").toString(), 100, "10m", true, randomBoolean()); licenseState = mock(XPackLicenseState.class); when(licenseState.isAuthorizationRealmAllowed()).thenReturn(true); @@ -177,4 +182,49 @@ public abstract class KerberosRealmTestCase extends ESTestCase { } return principalName; } + + /** + * Write content to provided keytab file. + * + * @param keytabPath {@link Path} to keytab file. + * @param content Content for keytab + * @return key tab path + * @throws IOException if I/O error occurs while writing keytab file + */ + public static Path writeKeyTab(final Path keytabPath, final String content) throws IOException { + try (BufferedWriter bufferedWriter = Files.newBufferedWriter(keytabPath, StandardCharsets.US_ASCII)) { + bufferedWriter.write(Strings.isNullOrEmpty(content) ? "test-content" : content); + } + return keytabPath; + } + + /** + * Build kerberos realm settings with default config and given keytab + * + * @param keytabPath key tab file path + * @return {@link Settings} for kerberos realm + */ + public static Settings buildKerberosRealmSettings(final String keytabPath) { + return buildKerberosRealmSettings(keytabPath, 100, "10m", true, false); + } + + /** + * Build kerberos realm settings + * + * @param keytabPath key tab file path + * @param maxUsersInCache max users to be maintained in cache + * @param cacheTTL time to live for cached entries + * @param enableDebugging for krb5 logs + * @param removeRealmName {@code true} if we want to remove realm name from the username of form 'user@REALM' + * @return {@link Settings} for kerberos realm + */ + public static Settings buildKerberosRealmSettings(final String keytabPath, final int maxUsersInCache, final String cacheTTL, + final boolean enableDebugging, final boolean removeRealmName) { + final Settings.Builder builder = Settings.builder().put(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.getKey(), keytabPath) + .put(KerberosRealmSettings.CACHE_MAX_USERS_SETTING.getKey(), maxUsersInCache) + .put(KerberosRealmSettings.CACHE_TTL_SETTING.getKey(), cacheTTL) + .put(KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.getKey(), enableDebugging) + .put(KerberosRealmSettings.SETTING_REMOVE_REALM_NAME.getKey(), removeRealmName); + return builder.build(); + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTests.java index d35068fd07a..1166e929341 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTests.java @@ -155,7 +155,7 @@ public class KerberosRealmTests extends KerberosRealmTestCase { } private void assertKerberosRealmConstructorFails(final String keytabPath, final String expectedErrorMessage) { - settings = KerberosTestCase.buildKerberosRealmSettings(keytabPath, 100, "10m", true, randomBoolean()); + settings = buildKerberosRealmSettings(keytabPath, 100, "10m", true, randomBoolean()); config = new RealmConfig("test-kerb-realm", settings, globalSettings, TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)); mockNativeRoleMappingStore = roleMappingStore(Arrays.asList("user")); diff --git a/x-pack/qa/evil-tests/build.gradle b/x-pack/qa/evil-tests/build.gradle index 03f2a569873..d411909fb31 100644 --- a/x-pack/qa/evil-tests/build.gradle +++ b/x-pack/qa/evil-tests/build.gradle @@ -1,9 +1,11 @@ apply plugin: 'elasticsearch.standalone-test' dependencies { - testCompile "org.elasticsearch.plugin:x-pack-core:${version}" + testCompile project(path: xpackModule('core'), configuration: 'testArtifacts') + testCompile project(path: xpackModule('security'), configuration: 'testArtifacts') } test { systemProperty 'tests.security.manager', 'false' + include '**/*Tests.class' } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTestCase.java b/x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTestCase.java similarity index 73% rename from x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTestCase.java rename to x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTestCase.java index f97afc1d52c..f8795e6b4da 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTestCase.java +++ b/x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTestCase.java @@ -9,20 +9,15 @@ package org.elasticsearch.xpack.security.authc.kerberos; import org.apache.logging.log4j.Logger; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.common.Randomness; -import org.elasticsearch.common.Strings; import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; -import java.io.BufferedWriter; import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.nio.file.Path; import java.security.AccessController; import java.security.PrivilegedActionException; @@ -130,12 +125,14 @@ public abstract class KerberosTestCase extends ESTestCase { throw ExceptionsHelper.convertToRuntime(e); } }); - settings = buildKerberosRealmSettings(ktabPathForService.toString()); + settings = KerberosRealmTestCase.buildKerberosRealmSettings(ktabPathForService.toString()); } @After public void tearDownMiniKdc() throws IOException, PrivilegedActionException { - simpleKdcLdapServer.stop(); + if (simpleKdcLdapServer != null) { + simpleKdcLdapServer.stop(); + } } /** @@ -186,49 +183,4 @@ public abstract class KerberosTestCase extends ESTestCase { return AccessController.doPrivileged((PrivilegedExceptionAction) () -> Subject.doAs(subject, action)); } - /** - * Write content to provided keytab file. - * - * @param keytabPath {@link Path} to keytab file. - * @param content Content for keytab - * @return key tab path - * @throws IOException if I/O error occurs while writing keytab file - */ - public static Path writeKeyTab(final Path keytabPath, final String content) throws IOException { - try (BufferedWriter bufferedWriter = Files.newBufferedWriter(keytabPath, StandardCharsets.US_ASCII)) { - bufferedWriter.write(Strings.isNullOrEmpty(content) ? "test-content" : content); - } - return keytabPath; - } - - /** - * Build kerberos realm settings with default config and given keytab - * - * @param keytabPath key tab file path - * @return {@link Settings} for kerberos realm - */ - public static Settings buildKerberosRealmSettings(final String keytabPath) { - return buildKerberosRealmSettings(keytabPath, 100, "10m", true, false); - } - - /** - * Build kerberos realm settings - * - * @param keytabPath key tab file path - * @param maxUsersInCache max users to be maintained in cache - * @param cacheTTL time to live for cached entries - * @param enableDebugging for krb5 logs - * @param removeRealmName {@code true} if we want to remove realm name from the username of form 'user@REALM' - * @return {@link Settings} for kerberos realm - */ - public static Settings buildKerberosRealmSettings(final String keytabPath, final int maxUsersInCache, final String cacheTTL, - final boolean enableDebugging, final boolean removeRealmName) { - final Settings.Builder builder = Settings.builder().put(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.getKey(), keytabPath) - .put(KerberosRealmSettings.CACHE_MAX_USERS_SETTING.getKey(), maxUsersInCache) - .put(KerberosRealmSettings.CACHE_TTL_SETTING.getKey(), cacheTTL) - .put(KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.getKey(), enableDebugging) - .put(KerberosRealmSettings.SETTING_REMOVE_REALM_NAME.getKey(), removeRealmName); - return builder.build(); - } - } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTicketValidatorTests.java b/x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTicketValidatorTests.java similarity index 96% rename from x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTicketValidatorTests.java rename to x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTicketValidatorTests.java index 8f35e0bde44..340d05ce35e 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTicketValidatorTests.java +++ b/x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTicketValidatorTests.java @@ -86,8 +86,8 @@ public class KerberosTicketValidatorTests extends KerberosTestCase { final String base64KerbToken = spnegoClient.getBase64EncodedTokenForSpnegoHeader(); assertThat(base64KerbToken, is(notNullValue())); - final Path ktabPath = writeKeyTab(workDir.resolve("invalid.keytab"), "not - a - valid - key - tab"); - settings = buildKerberosRealmSettings(ktabPath.toString()); + final Path ktabPath = KerberosRealmTestCase.writeKeyTab(workDir.resolve("invalid.keytab"), "not - a - valid - key - tab"); + settings = KerberosRealmTestCase.buildKerberosRealmSettings(ktabPath.toString()); final Environment env = TestEnvironment.newEnvironment(globalSettings); final Path keytabPath = env.configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(settings)); final PlainActionFuture> future = new PlainActionFuture<>(); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServer.java b/x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServer.java similarity index 100% rename from x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServer.java rename to x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServer.java diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServerTests.java b/x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServerTests.java similarity index 100% rename from x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServerTests.java rename to x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServerTests.java diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SpnegoClient.java b/x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SpnegoClient.java similarity index 100% rename from x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SpnegoClient.java rename to x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SpnegoClient.java diff --git a/x-pack/plugin/security/src/test/resources/kdc.ldiff b/x-pack/qa/evil-tests/src/test/resources/kdc.ldiff similarity index 100% rename from x-pack/plugin/security/src/test/resources/kdc.ldiff rename to x-pack/qa/evil-tests/src/test/resources/kdc.ldiff From 189aaceecf764d5ec7b34cd26e27ae6f621fe4cf Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Thu, 13 Sep 2018 22:15:21 -0400 Subject: [PATCH 69/78] AwaitsFix testRestoreMinmal Tracked at #33689 --- .../elasticsearch/snapshots/SourceOnlySnapshotShardTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotShardTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotShardTests.java index 7058724ecf0..261133b8907 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotShardTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotShardTests.java @@ -162,6 +162,7 @@ public class SourceOnlySnapshotShardTests extends IndexShardTestCase { return "{ \"value\" : \"" + randomAlphaOfLength(10) + "\"}"; } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/33689") public void testRestoreMinmal() throws IOException { IndexShard shard = newStartedShard(true); int numInitialDocs = randomIntBetween(10, 100); From 0b4960ff6b93d94dae824156dddbf0084f8ea1b3 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Fri, 14 Sep 2018 06:21:18 +0200 Subject: [PATCH 70/78] SCRIPTING: Move terms_set Context to its Own Class (#33602) * SCRIPTING: Move terms_set Context to its Own Class * Extracted TermsSetQueryScript * Kept mechanics close to what they were with SearchScript --- .../index/query/TermsSetQueryBuilder.java | 21 ++-- .../elasticsearch/script/ParameterMap.java | 105 ++++++++++++++++ .../elasticsearch/script/ScriptModule.java | 2 +- .../elasticsearch/script/SearchScript.java | 2 - .../script/TermsSetQueryScript.java | 112 ++++++++++++++++++ .../script/MockScriptEngine.java | 12 ++ 6 files changed, 240 insertions(+), 14 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/script/ParameterMap.java create mode 100644 server/src/main/java/org/elasticsearch/script/TermsSetQueryScript.java diff --git a/server/src/main/java/org/elasticsearch/index/query/TermsSetQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/TermsSetQueryBuilder.java index c20df00a109..1e151896df0 100644 --- a/server/src/main/java/org/elasticsearch/index/query/TermsSetQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/TermsSetQueryBuilder.java @@ -40,7 +40,6 @@ import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.fielddata.IndexNumericFieldData; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.script.Script; -import org.elasticsearch.script.SearchScript; import java.io.IOException; import java.util.ArrayList; @@ -48,6 +47,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import org.elasticsearch.script.TermsSetQueryScript; public final class TermsSetQueryBuilder extends AbstractQueryBuilder { @@ -262,13 +262,12 @@ public final class TermsSetQueryBuilder extends AbstractQueryBuilder params = new HashMap<>(); params.putAll(minimumShouldMatchScript.getParams()); params.put("num_terms", values.size()); - SearchScript.LeafFactory leafFactory = factory.newFactory(params, context.lookup()); - longValuesSource = new ScriptLongValueSource(minimumShouldMatchScript, leafFactory); + longValuesSource = new ScriptLongValueSource(minimumShouldMatchScript, factory.newFactory(params, context.lookup())); } else { throw new IllegalStateException("No minimum should match has been specified"); } @@ -278,26 +277,26 @@ public final class TermsSetQueryBuilder extends AbstractQueryBuilder { + + private static final DeprecationLogger DEPRECATION_LOGGER = + new DeprecationLogger(LogManager.getLogger(ParameterMap.class)); + + private final Map params; + + private final Map deprecations; + + ParameterMap(Map params, Map deprecations) { + this.params = params; + this.deprecations = deprecations; + } + + @Override + public int size() { + return params.size(); + } + + @Override + public boolean isEmpty() { + return params.isEmpty(); + } + + @Override + public boolean containsKey(final Object key) { + return params.containsKey(key); + } + + @Override + public boolean containsValue(final Object value) { + return params.containsValue(value); + } + + @Override + public Object get(final Object key) { + String deprecationMessage = deprecations.get(key); + if (deprecationMessage != null) { + DEPRECATION_LOGGER.deprecated(deprecationMessage); + } + return params.get(key); + } + + @Override + public Object put(final String key, final Object value) { + return params.put(key, value); + } + + @Override + public Object remove(final Object key) { + return params.remove(key); + } + + @Override + public void putAll(final Map m) { + params.putAll(m); + } + + @Override + public void clear() { + params.clear(); + } + + @Override + public Set keySet() { + return params.keySet(); + } + + @Override + public Collection values() { + return params.values(); + } + + @Override + public Set> entrySet() { + return params.entrySet(); + } +} diff --git a/server/src/main/java/org/elasticsearch/script/ScriptModule.java b/server/src/main/java/org/elasticsearch/script/ScriptModule.java index 6dc507fa0d8..968bc143ba8 100644 --- a/server/src/main/java/org/elasticsearch/script/ScriptModule.java +++ b/server/src/main/java/org/elasticsearch/script/ScriptModule.java @@ -44,7 +44,7 @@ public class ScriptModule { SearchScript.AGGS_CONTEXT, ScoreScript.CONTEXT, SearchScript.SCRIPT_SORT_CONTEXT, - SearchScript.TERMS_SET_QUERY_CONTEXT, + TermsSetQueryScript.CONTEXT, ExecutableScript.CONTEXT, UpdateScript.CONTEXT, BucketAggregationScript.CONTEXT, diff --git a/server/src/main/java/org/elasticsearch/script/SearchScript.java b/server/src/main/java/org/elasticsearch/script/SearchScript.java index fb5f950d61d..cdf5c98ec62 100644 --- a/server/src/main/java/org/elasticsearch/script/SearchScript.java +++ b/server/src/main/java/org/elasticsearch/script/SearchScript.java @@ -149,6 +149,4 @@ public abstract class SearchScript implements ScorerAware, ExecutableScript { public static final ScriptContext AGGS_CONTEXT = new ScriptContext<>("aggs", Factory.class); // Can return a double. (For ScriptSortType#NUMBER only, for ScriptSortType#STRING normal CONTEXT should be used) public static final ScriptContext SCRIPT_SORT_CONTEXT = new ScriptContext<>("sort", Factory.class); - // Can return a long - public static final ScriptContext TERMS_SET_QUERY_CONTEXT = new ScriptContext<>("terms_set", Factory.class); } diff --git a/server/src/main/java/org/elasticsearch/script/TermsSetQueryScript.java b/server/src/main/java/org/elasticsearch/script/TermsSetQueryScript.java new file mode 100644 index 00000000000..085f40e0d7a --- /dev/null +++ b/server/src/main/java/org/elasticsearch/script/TermsSetQueryScript.java @@ -0,0 +1,112 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.script; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.apache.lucene.index.LeafReaderContext; +import org.elasticsearch.index.fielddata.ScriptDocValues; +import org.elasticsearch.search.lookup.LeafSearchLookup; +import org.elasticsearch.search.lookup.SearchLookup; + +public abstract class TermsSetQueryScript { + + public static final String[] PARAMETERS = {}; + + public static final ScriptContext CONTEXT = new ScriptContext<>("terms_set", Factory.class); + + private static final Map DEPRECATIONS; + + static { + Map deprecations = new HashMap<>(); + deprecations.put( + "doc", + "Accessing variable [doc] via [params.doc] from within a terms-set-query-script " + + "is deprecated in favor of directly accessing [doc]." + ); + deprecations.put( + "_doc", + "Accessing variable [doc] via [params._doc] from within a terms-set-query-script " + + "is deprecated in favor of directly accessing [doc]." + ); + DEPRECATIONS = Collections.unmodifiableMap(deprecations); + } + + /** + * The generic runtime parameters for the script. + */ + private final Map params; + + /** + * A leaf lookup for the bound segment this script will operate on. + */ + private final LeafSearchLookup leafLookup; + + public TermsSetQueryScript(Map params, SearchLookup lookup, LeafReaderContext leafContext) { + this.params = new ParameterMap(params, DEPRECATIONS); + this.leafLookup = lookup.getLeafSearchLookup(leafContext); + } + + /** + * Return the parameters for this script. + */ + public Map getParams() { + this.params.putAll(leafLookup.asMap()); + return params; + } + + /** + * The doc lookup for the Lucene segment this script was created for. + */ + public Map> getDoc() { + return leafLookup.doc(); + } + + /** + * Set the current document to run the script on next. + */ + public void setDocument(int docid) { + leafLookup.setDocument(docid); + } + + /** + * Return the result as a long. This is used by aggregation scripts over long fields. + */ + public long runAsLong() { + return execute().longValue(); + } + + public abstract Number execute(); + + /** + * A factory to construct {@link TermsSetQueryScript} instances. + */ + public interface LeafFactory { + TermsSetQueryScript newInstance(LeafReaderContext ctx) throws IOException; + } + + /** + * A factory to construct stateful {@link TermsSetQueryScript} factories for a specific index. + */ + public interface Factory { + LeafFactory newFactory(Map params, SearchLookup lookup); + } +} diff --git a/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java b/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java index be77846b2ba..be38ae95a32 100644 --- a/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java +++ b/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java @@ -85,6 +85,18 @@ public class MockScriptEngine implements ScriptEngine { if (context.instanceClazz.equals(SearchScript.class)) { SearchScript.Factory factory = mockCompiled::createSearchScript; return context.factoryClazz.cast(factory); + } else if(context.instanceClazz.equals(TermsSetQueryScript.class)) { + TermsSetQueryScript.Factory factory = (parameters, lookup) -> (TermsSetQueryScript.LeafFactory) ctx + -> new TermsSetQueryScript(parameters, lookup, ctx) { + @Override + public Number execute() { + Map vars = new HashMap<>(parameters); + vars.put("params", parameters); + vars.put("doc", getDoc()); + return (Number) script.apply(vars); + } + }; + return context.factoryClazz.cast(factory); } else if (context.instanceClazz.equals(ExecutableScript.class)) { ExecutableScript.Factory factory = mockCompiled::createExecutableScript; return context.factoryClazz.cast(factory); From 736053c6586fa4864e65423df06df25ce8b6949c Mon Sep 17 00:00:00 2001 From: Costin Leau Date: Fri, 14 Sep 2018 09:05:34 +0300 Subject: [PATCH 71/78] SQL: Return functions in JDBC driver metadata (#33672) Update JDBC database metadata to advertised the supported functions Add aliases to some date/time functions to match the ODBC spec Fix #33671 --- .../sql/jdbc/jdbc/JdbcDatabaseMetaData.java | 38 +++- .../expression/function/FunctionRegistry.java | 8 +- .../xpack/qa/sql/cli/ShowTestCase.java | 3 + .../sql/src/main/resources/command.csv-spec | 199 +++++++++--------- .../qa/sql/src/main/resources/docs.csv-spec | 186 ++++++++-------- 5 files changed, 239 insertions(+), 195 deletions(-) diff --git a/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcDatabaseMetaData.java b/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcDatabaseMetaData.java index d2e24f3edac..085016bc0bd 100644 --- a/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcDatabaseMetaData.java +++ b/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcDatabaseMetaData.java @@ -179,14 +179,34 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper { @Override public String getNumericFunctions() throws SQLException { - // TODO: sync this with the grammar - return ""; + //https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/numeric-functions?view=sql-server-2017 + return "ABS,ACOS,ASIN,ATAN,ATAN2," + + "CEILING,COS," + + "DEGREES," + + "EXP," + + "FLOOR," + + "LOG,LOG10," + + "MOD," + + "PI,POWER," + + "RADIANS,RAND,ROUND," + + "SIGN,SIN,SQRT," + + "TAN"; } @Override public String getStringFunctions() throws SQLException { - // TODO: sync this with the grammar - return ""; + //https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/string-functions?view=sql-server-2017 + return "ASCII," + + "BIT_LENGTH," + + "CHAR,CHAR_LENGTH,CHARACTER_LENGTH,CONCAT," + + "INSERT," + + "LCASE,LEFT,LENGTH,LOCATE,LTRIM," + // waiting on https://github.com/elastic/elasticsearch/issues/33477 + //+ "OCTET_LENGTH," + + "POSITION," + + "REPEAT,REPLACE,RIGHT,RTRIM," + + "SPACE,SUBSTRING," + + "UCASE"; } @Override @@ -197,7 +217,15 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper { @Override public String getTimeDateFunctions() throws SQLException { - return ""; + //https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/time-date-and-interval-functions?view=sql-server-2017 + return "DAYNAME,DAYOFMONTH,DAYOFWEEK,DAYOFYEAR" + + "EXTRACT," + + "HOUR," + + "MINUTE,MONTH,MONTHNAME" + + "QUARTER," + + "SECOND," + + "WEEK," + + "YEAR"; } @Override diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/FunctionRegistry.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/FunctionRegistry.java index 820aafb0116..2daa90c7bda 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/FunctionRegistry.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/FunctionRegistry.java @@ -116,9 +116,9 @@ public class FunctionRegistry { def(Kurtosis.class, Kurtosis::new), // Scalar functions // Date - def(DayOfMonth.class, DayOfMonth::new, "DAY", "DOM"), - def(DayOfWeek.class, DayOfWeek::new, "DOW"), - def(DayOfYear.class, DayOfYear::new, "DOY"), + def(DayOfMonth.class, DayOfMonth::new, "DAYOFMONTH", "DAY", "DOM"), + def(DayOfWeek.class, DayOfWeek::new, "DAYOFWEEK", "DOW"), + def(DayOfYear.class, DayOfYear::new, "DAYOFYEAR", "DOY"), def(HourOfDay.class, HourOfDay::new, "HOUR"), def(MinuteOfDay.class, MinuteOfDay::new), def(MinuteOfHour.class, MinuteOfHour::new, "MINUTE"), @@ -163,7 +163,7 @@ public class FunctionRegistry { def(Ascii.class, Ascii::new), def(Char.class, Char::new), def(BitLength.class, BitLength::new), - def(CharLength.class, CharLength::new), + def(CharLength.class, CharLength::new, "CHARACTER_LENGTH"), def(LCase.class, LCase::new), def(Length.class, Length::new), def(LTrim.class, LTrim::new), diff --git a/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/ShowTestCase.java b/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/ShowTestCase.java index 601dca8abd4..8dbd4b187f7 100644 --- a/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/ShowTestCase.java +++ b/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/ShowTestCase.java @@ -60,9 +60,12 @@ public abstract class ShowTestCase extends CliIntegrationTestCase { assertThat(command("SHOW FUNCTIONS LIKE '%DAY%'"), RegexMatcher.matches("\\s*name\\s*\\|\\s*type\\s*")); assertThat(readLine(), containsString("----------")); assertThat(readLine(), RegexMatcher.matches("\\s*DAY_OF_MONTH\\s*\\|\\s*SCALAR\\s*")); + assertThat(readLine(), RegexMatcher.matches("\\s*DAYOFMONTH\\s*\\|\\s*SCALAR\\s*")); assertThat(readLine(), RegexMatcher.matches("\\s*DAY\\s*\\|\\s*SCALAR\\s*")); assertThat(readLine(), RegexMatcher.matches("\\s*DAY_OF_WEEK\\s*\\|\\s*SCALAR\\s*")); + assertThat(readLine(), RegexMatcher.matches("\\s*DAYOFWEEK\\s*\\|\\s*SCALAR\\s*")); assertThat(readLine(), RegexMatcher.matches("\\s*DAY_OF_YEAR\\s*\\|\\s*SCALAR\\s*")); + assertThat(readLine(), RegexMatcher.matches("\\s*DAYOFYEAR\\s*\\|\\s*SCALAR\\s*")); assertThat(readLine(), RegexMatcher.matches("\\s*HOUR_OF_DAY\\s*\\|\\s*SCALAR\\s*")); assertThat(readLine(), RegexMatcher.matches("\\s*MINUTE_OF_DAY\\s*\\|\\s*SCALAR\\s*")); assertThat(readLine(), RegexMatcher.matches("\\s*DAY_NAME\\s*\\|\\s*SCALAR\\s*")); diff --git a/x-pack/qa/sql/src/main/resources/command.csv-spec b/x-pack/qa/sql/src/main/resources/command.csv-spec index 28aadeded2c..81aa18b2e84 100644 --- a/x-pack/qa/sql/src/main/resources/command.csv-spec +++ b/x-pack/qa/sql/src/main/resources/command.csv-spec @@ -7,93 +7,97 @@ showFunctions SHOW FUNCTIONS; name:s | type:s -AVG |AGGREGATE -COUNT |AGGREGATE -MAX |AGGREGATE -MIN |AGGREGATE -SUM |AGGREGATE -STDDEV_POP |AGGREGATE -VAR_POP |AGGREGATE -PERCENTILE |AGGREGATE -PERCENTILE_RANK |AGGREGATE -SUM_OF_SQUARES |AGGREGATE -SKEWNESS |AGGREGATE -KURTOSIS |AGGREGATE -DAY_OF_MONTH |SCALAR -DAY |SCALAR -DOM |SCALAR -DAY_OF_WEEK |SCALAR -DOW |SCALAR -DAY_OF_YEAR |SCALAR -DOY |SCALAR -HOUR_OF_DAY |SCALAR -HOUR |SCALAR -MINUTE_OF_DAY |SCALAR -MINUTE_OF_HOUR |SCALAR -MINUTE |SCALAR -SECOND_OF_MINUTE|SCALAR -SECOND |SCALAR -MONTH_OF_YEAR |SCALAR -MONTH |SCALAR -YEAR |SCALAR -WEEK_OF_YEAR |SCALAR -WEEK |SCALAR -DAY_NAME |SCALAR -DAYNAME |SCALAR -MONTH_NAME |SCALAR -MONTHNAME |SCALAR -QUARTER |SCALAR -ABS |SCALAR -ACOS |SCALAR -ASIN |SCALAR -ATAN |SCALAR -ATAN2 |SCALAR -CBRT |SCALAR -CEIL |SCALAR -CEILING |SCALAR -COS |SCALAR -COSH |SCALAR -COT |SCALAR -DEGREES |SCALAR -E |SCALAR -EXP |SCALAR -EXPM1 |SCALAR -FLOOR |SCALAR -LOG |SCALAR -LOG10 |SCALAR -MOD |SCALAR -PI |SCALAR -POWER |SCALAR -RADIANS |SCALAR -RANDOM |SCALAR -RAND |SCALAR -ROUND |SCALAR -SIGN |SCALAR -SIGNUM |SCALAR -SIN |SCALAR -SINH |SCALAR -SQRT |SCALAR -TAN |SCALAR -ASCII |SCALAR -CHAR |SCALAR -BIT_LENGTH |SCALAR -CHAR_LENGTH |SCALAR -LCASE |SCALAR -LENGTH |SCALAR -LTRIM |SCALAR -RTRIM |SCALAR -SPACE |SCALAR -CONCAT |SCALAR -INSERT |SCALAR -LEFT |SCALAR -LOCATE |SCALAR -POSITION |SCALAR -REPEAT |SCALAR -REPLACE |SCALAR -RIGHT |SCALAR -SUBSTRING |SCALAR -UCASE |SCALAR -SCORE |SCORE +AVG |AGGREGATE +COUNT |AGGREGATE +MAX |AGGREGATE +MIN |AGGREGATE +SUM |AGGREGATE +STDDEV_POP |AGGREGATE +VAR_POP |AGGREGATE +PERCENTILE |AGGREGATE +PERCENTILE_RANK |AGGREGATE +SUM_OF_SQUARES |AGGREGATE +SKEWNESS |AGGREGATE +KURTOSIS |AGGREGATE +DAY_OF_MONTH |SCALAR +DAYOFMONTH |SCALAR +DAY |SCALAR +DOM |SCALAR +DAY_OF_WEEK |SCALAR +DAYOFWEEK |SCALAR +DOW |SCALAR +DAY_OF_YEAR |SCALAR +DAYOFYEAR |SCALAR +DOY |SCALAR +HOUR_OF_DAY |SCALAR +HOUR |SCALAR +MINUTE_OF_DAY |SCALAR +MINUTE_OF_HOUR |SCALAR +MINUTE |SCALAR +SECOND_OF_MINUTE|SCALAR +SECOND |SCALAR +MONTH_OF_YEAR |SCALAR +MONTH |SCALAR +YEAR |SCALAR +WEEK_OF_YEAR |SCALAR +WEEK |SCALAR +DAY_NAME |SCALAR +DAYNAME |SCALAR +MONTH_NAME |SCALAR +MONTHNAME |SCALAR +QUARTER |SCALAR +ABS |SCALAR +ACOS |SCALAR +ASIN |SCALAR +ATAN |SCALAR +ATAN2 |SCALAR +CBRT |SCALAR +CEIL |SCALAR +CEILING |SCALAR +COS |SCALAR +COSH |SCALAR +COT |SCALAR +DEGREES |SCALAR +E |SCALAR +EXP |SCALAR +EXPM1 |SCALAR +FLOOR |SCALAR +LOG |SCALAR +LOG10 |SCALAR +MOD |SCALAR +PI |SCALAR +POWER |SCALAR +RADIANS |SCALAR +RANDOM |SCALAR +RAND |SCALAR +ROUND |SCALAR +SIGN |SCALAR +SIGNUM |SCALAR +SIN |SCALAR +SINH |SCALAR +SQRT |SCALAR +TAN |SCALAR +ASCII |SCALAR +CHAR |SCALAR +BIT_LENGTH |SCALAR +CHAR_LENGTH |SCALAR +CHARACTER_LENGTH|SCALAR +LCASE |SCALAR +LENGTH |SCALAR +LTRIM |SCALAR +RTRIM |SCALAR +SPACE |SCALAR +CONCAT |SCALAR +INSERT |SCALAR +LEFT |SCALAR +LOCATE |SCALAR +POSITION |SCALAR +REPEAT |SCALAR +REPLACE |SCALAR +RIGHT |SCALAR +SUBSTRING |SCALAR +UCASE |SCALAR +SCORE |SCORE ; showFunctionsWithExactMatch @@ -128,15 +132,18 @@ ABS |SCALAR showFunctionsWithLeadingPattern SHOW FUNCTIONS LIKE '%DAY%'; - name:s | type:s -DAY_OF_MONTH |SCALAR -DAY |SCALAR -DAY_OF_WEEK |SCALAR -DAY_OF_YEAR |SCALAR -HOUR_OF_DAY |SCALAR -MINUTE_OF_DAY |SCALAR -DAY_NAME |SCALAR -DAYNAME |SCALAR + name:s | type:s +DAY_OF_MONTH |SCALAR +DAYOFMONTH |SCALAR +DAY |SCALAR +DAY_OF_WEEK |SCALAR +DAYOFWEEK |SCALAR +DAY_OF_YEAR |SCALAR +DAYOFYEAR |SCALAR +HOUR_OF_DAY |SCALAR +MINUTE_OF_DAY |SCALAR +DAY_NAME |SCALAR +DAYNAME |SCALAR ; showTables diff --git a/x-pack/qa/sql/src/main/resources/docs.csv-spec b/x-pack/qa/sql/src/main/resources/docs.csv-spec index 52356bdfd52..280e9a5edf0 100644 --- a/x-pack/qa/sql/src/main/resources/docs.csv-spec +++ b/x-pack/qa/sql/src/main/resources/docs.csv-spec @@ -183,94 +183,97 @@ SHOW FUNCTIONS; name | type ----------------+--------------- -AVG |AGGREGATE -COUNT |AGGREGATE -MAX |AGGREGATE -MIN |AGGREGATE -SUM |AGGREGATE -STDDEV_POP |AGGREGATE -VAR_POP |AGGREGATE -PERCENTILE |AGGREGATE -PERCENTILE_RANK |AGGREGATE -SUM_OF_SQUARES |AGGREGATE -SKEWNESS |AGGREGATE -KURTOSIS |AGGREGATE -DAY_OF_MONTH |SCALAR -DAY |SCALAR -DOM |SCALAR -DAY_OF_WEEK |SCALAR -DOW |SCALAR -DAY_OF_YEAR |SCALAR -DOY |SCALAR -HOUR_OF_DAY |SCALAR -HOUR |SCALAR -MINUTE_OF_DAY |SCALAR -MINUTE_OF_HOUR |SCALAR -MINUTE |SCALAR -SECOND_OF_MINUTE|SCALAR -SECOND |SCALAR -MONTH_OF_YEAR |SCALAR -MONTH |SCALAR -YEAR |SCALAR -WEEK_OF_YEAR |SCALAR -WEEK |SCALAR -DAY_NAME |SCALAR -DAYNAME |SCALAR -MONTH_NAME |SCALAR -MONTHNAME |SCALAR -QUARTER |SCALAR -ABS |SCALAR -ACOS |SCALAR -ASIN |SCALAR -ATAN |SCALAR -ATAN2 |SCALAR -CBRT |SCALAR -CEIL |SCALAR -CEILING |SCALAR -COS |SCALAR -COSH |SCALAR -COT |SCALAR -DEGREES |SCALAR -E |SCALAR -EXP |SCALAR -EXPM1 |SCALAR -FLOOR |SCALAR -LOG |SCALAR -LOG10 |SCALAR -MOD |SCALAR -PI |SCALAR -POWER |SCALAR -RADIANS |SCALAR -RANDOM |SCALAR -RAND |SCALAR -ROUND |SCALAR -SIGN |SCALAR -SIGNUM |SCALAR -SIN |SCALAR -SINH |SCALAR -SQRT |SCALAR -TAN |SCALAR -ASCII |SCALAR -CHAR |SCALAR -BIT_LENGTH |SCALAR +AVG |AGGREGATE +COUNT |AGGREGATE +MAX |AGGREGATE +MIN |AGGREGATE +SUM |AGGREGATE +STDDEV_POP |AGGREGATE +VAR_POP |AGGREGATE +PERCENTILE |AGGREGATE +PERCENTILE_RANK |AGGREGATE +SUM_OF_SQUARES |AGGREGATE +SKEWNESS |AGGREGATE +KURTOSIS |AGGREGATE +DAY_OF_MONTH |SCALAR +DAYOFMONTH |SCALAR +DAY |SCALAR +DOM |SCALAR +DAY_OF_WEEK |SCALAR +DAYOFWEEK |SCALAR +DOW |SCALAR +DAY_OF_YEAR |SCALAR +DAYOFYEAR |SCALAR +DOY |SCALAR +HOUR_OF_DAY |SCALAR +HOUR |SCALAR +MINUTE_OF_DAY |SCALAR +MINUTE_OF_HOUR |SCALAR +MINUTE |SCALAR +SECOND_OF_MINUTE|SCALAR +SECOND |SCALAR +MONTH_OF_YEAR |SCALAR +MONTH |SCALAR +YEAR |SCALAR +WEEK_OF_YEAR |SCALAR +WEEK |SCALAR +DAY_NAME |SCALAR +DAYNAME |SCALAR +MONTH_NAME |SCALAR +MONTHNAME |SCALAR +QUARTER |SCALAR +ABS |SCALAR +ACOS |SCALAR +ASIN |SCALAR +ATAN |SCALAR +ATAN2 |SCALAR +CBRT |SCALAR +CEIL |SCALAR +CEILING |SCALAR +COS |SCALAR +COSH |SCALAR +COT |SCALAR +DEGREES |SCALAR +E |SCALAR +EXP |SCALAR +EXPM1 |SCALAR +FLOOR |SCALAR +LOG |SCALAR +LOG10 |SCALAR +MOD |SCALAR +PI |SCALAR +POWER |SCALAR +RADIANS |SCALAR +RANDOM |SCALAR +RAND |SCALAR +ROUND |SCALAR +SIGN |SCALAR +SIGNUM |SCALAR +SIN |SCALAR +SINH |SCALAR +SQRT |SCALAR +TAN |SCALAR +ASCII |SCALAR +CHAR |SCALAR +BIT_LENGTH |SCALAR CHAR_LENGTH |SCALAR -LCASE |SCALAR -LENGTH |SCALAR -LTRIM |SCALAR -RTRIM |SCALAR -SPACE |SCALAR -CONCAT |SCALAR -INSERT |SCALAR -LEFT |SCALAR -LOCATE |SCALAR -POSITION |SCALAR -REPEAT |SCALAR -REPLACE |SCALAR -RIGHT |SCALAR -SUBSTRING |SCALAR -UCASE |SCALAR -SCORE |SCORE - +CHARACTER_LENGTH|SCALAR +LCASE |SCALAR +LENGTH |SCALAR +LTRIM |SCALAR +RTRIM |SCALAR +SPACE |SCALAR +CONCAT |SCALAR +INSERT |SCALAR +LEFT |SCALAR +LOCATE |SCALAR +POSITION |SCALAR +REPEAT |SCALAR +REPLACE |SCALAR +RIGHT |SCALAR +SUBSTRING |SCALAR +UCASE |SCALAR +SCORE |SCORE // end::showFunctions ; @@ -319,13 +322,16 @@ SHOW FUNCTIONS LIKE '%DAY%'; name | type ---------------+--------------- DAY_OF_MONTH |SCALAR +DAYOFMONTH |SCALAR DAY |SCALAR DAY_OF_WEEK |SCALAR +DAYOFWEEK |SCALAR DAY_OF_YEAR |SCALAR +DAYOFYEAR |SCALAR HOUR_OF_DAY |SCALAR -MINUTE_OF_DAY |SCALAR -DAY_NAME |SCALAR -DAYNAME |SCALAR +MINUTE_OF_DAY |SCALAR +DAY_NAME |SCALAR +DAYNAME |SCALAR // end::showFunctionsWithPattern ; From 8ae1eeb3039efae9048e92362bfbde91c95d48d4 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Fri, 14 Sep 2018 09:42:03 +0300 Subject: [PATCH 72/78] [TESTS] Disable specific locales for RestrictedTrustManagerTest (#33299) Disable specific Thai and Japanese locales as Certificate expiration validation fails due to the date parsing of BouncyCastle (that manifests in a FIPS 140 JVM as this is the only place we use BouncyCastle). Added the locale switching logic here instead of subclassing ESTestCase as these are the only tests that fail for these locales and JVM combination. Resolves #33081 --- .../core/ssl/RestrictedTrustManagerTests.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustManagerTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustManagerTests.java index c1a39582e4f..24dc2d9847a 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustManagerTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustManagerTests.java @@ -5,12 +5,17 @@ */ package org.elasticsearch.xpack.core.ssl; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.test.ESTestCase; import org.hamcrest.Description; import org.hamcrest.TypeSafeMatcher; +import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; +import org.junit.BeforeClass; + import javax.net.ssl.X509ExtendedTrustManager; import java.io.IOException; @@ -28,6 +33,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.regex.Pattern; @@ -40,6 +46,34 @@ public class RestrictedTrustManagerTests extends ESTestCase { private int numberOfClusters; private int numberOfNodes; + private static Locale restoreLocale; + + @BeforeClass + public static void ensureSupportedLocale() throws Exception { + Logger logger = Loggers.getLogger(RestrictedTrustManagerTests.class); + if (isUnusableLocale()) { + // See: https://github.com/elastic/elasticsearch/issues/33081 + logger.warn("Attempting to run RestrictedTrustManagerTests tests in an unusable locale in a FIPS JVM. Certificate expiration " + + "validation will fail, switching to English"); + restoreLocale = Locale.getDefault(); + Locale.setDefault(Locale.ENGLISH); + } + } + + private static boolean isUnusableLocale() { + return inFipsJvm() && (Locale.getDefault().toLanguageTag().equals("th-TH") + || Locale.getDefault().toLanguageTag().equals("ja-JP-u-ca-japanese-x-lvariant-JP") + || Locale.getDefault().toLanguageTag().equals("th-TH-u-nu-thai-x-lvariant-TH")); + } + + @AfterClass + public static void restoreLocale() throws Exception { + if (restoreLocale != null) { + Locale.setDefault(restoreLocale); + restoreLocale = null; + } + } + @Before public void readCertificates() throws GeneralSecurityException, IOException { From d810f1b094e7629332d282ba3e403187ab7b17b3 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad <902768+bizybot@users.noreply.github.com> Date: Fri, 14 Sep 2018 17:17:53 +1000 Subject: [PATCH 73/78] [Kerberos] Add realm name & UPN to user metadata (#33338) We have a Kerberos setting to remove realm part from the user principal name (remove_realm_name). If this is true then the realm name is removed to form username but in the process, the realm name is lost. For scenarios like Kerberos cross-realm authentication, one could make use of the realm name to determine role mapping for users coming from different realms. This commit adds user metadata for kerberos_realm and kerberos_user_principal_name. --- .../configuring-kerberos-realm.asciidoc | 6 +++ .../authc/kerberos/KerberosRealm.java | 48 +++++++++---------- .../KerberosRealmAuthenticateFailedTests.java | 7 ++- .../kerberos/KerberosRealmCacheTests.java | 17 +++++-- .../authc/kerberos/KerberosRealmTestCase.java | 14 ++++++ .../authc/kerberos/KerberosRealmTests.java | 7 ++- 6 files changed, 70 insertions(+), 29 deletions(-) diff --git a/x-pack/docs/en/security/authentication/configuring-kerberos-realm.asciidoc b/x-pack/docs/en/security/authentication/configuring-kerberos-realm.asciidoc index 9e7ed476272..cc0863112c7 100644 --- a/x-pack/docs/en/security/authentication/configuring-kerberos-realm.asciidoc +++ b/x-pack/docs/en/security/authentication/configuring-kerberos-realm.asciidoc @@ -165,6 +165,12 @@ POST _xpack/security/role_mapping/kerbrolemapping -------------------------------------------------- // CONSOLE +In case you want to support Kerberos cross realm authentication you may +need to map roles based on the Kerberos realm name. For such scenarios +following are the additional user metadata available for role mapping: +- `kerberos_realm` will be set to Kerberos realm name. +- `kerberos_user_principal_name` will be set to user principal name from the Kerberos ticket. + For more information, see {stack-ov}/mapping-roles.html[Mapping users and groups to roles]. NOTE: The Kerberos realm supports diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealm.java index 9c531d3159f..0f47b6032f5 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealm.java @@ -30,6 +30,7 @@ import org.ietf.jgss.GSSException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -58,6 +59,9 @@ import static org.elasticsearch.xpack.security.authc.kerberos.KerberosAuthentica */ public final class KerberosRealm extends Realm implements CachingRealm { + public static final String KRB_METADATA_REALM_NAME_KEY = "kerberos_realm"; + public static final String KRB_METADATA_UPN_KEY = "kerberos_user_principal_name"; + private final Cache userPrincipalNameToUserCache; private final NativeRoleMappingStore userRoleMapper; private final KerberosTicketValidator kerberosTicketValidator; @@ -151,8 +155,7 @@ public final class KerberosRealm extends Realm implements CachingRealm { kerberosTicketValidator.validateTicket((byte[]) kerbAuthnToken.credentials(), keytabPath, enableKerberosDebug, ActionListener.wrap(userPrincipalNameOutToken -> { if (userPrincipalNameOutToken.v1() != null) { - final String username = maybeRemoveRealmName(userPrincipalNameOutToken.v1()); - resolveUser(username, userPrincipalNameOutToken.v2(), listener); + resolveUser(userPrincipalNameOutToken.v1(), userPrincipalNameOutToken.v2(), listener); } else { /** * This is when security context could not be established may be due to ongoing @@ -171,23 +174,8 @@ public final class KerberosRealm extends Realm implements CachingRealm { }, e -> handleException(e, listener))); } - /** - * Usually principal names are in the form 'user/instance@REALM'. This method - * removes '@REALM' part from the principal name if - * {@link KerberosRealmSettings#SETTING_REMOVE_REALM_NAME} is {@code true} else - * will return the input string. - * - * @param principalName user principal name - * @return username after removal of realm - */ - protected String maybeRemoveRealmName(final String principalName) { - if (this.removeRealmName) { - int foundAtIndex = principalName.indexOf('@'); - if (foundAtIndex > 0) { - return principalName.substring(0, foundAtIndex); - } - } - return principalName; + private String[] splitUserPrincipalName(final String userPrincipalName) { + return userPrincipalName.split("@"); } private void handleException(Exception e, final ActionListener listener) { @@ -205,13 +193,21 @@ public final class KerberosRealm extends Realm implements CachingRealm { } } - private void resolveUser(final String username, final String outToken, final ActionListener listener) { + private void resolveUser(final String userPrincipalName, final String outToken, final ActionListener listener) { // if outToken is present then it needs to be communicated with peer, add it to // response header in thread context. if (Strings.hasText(outToken)) { threadPool.getThreadContext().addResponseHeader(WWW_AUTHENTICATE, NEGOTIATE_AUTH_HEADER_PREFIX + outToken); } + final String[] userAndRealmName = splitUserPrincipalName(userPrincipalName); + /* + * Usually principal names are in the form 'user/instance@REALM'. If + * KerberosRealmSettings#SETTING_REMOVE_REALM_NAME is true then remove + * '@REALM' part from the user principal name to get username. + */ + final String username = (this.removeRealmName) ? userAndRealmName[0] : userPrincipalName; + if (delegatedRealms.hasDelegation()) { delegatedRealms.resolve(username, listener); } else { @@ -219,15 +215,19 @@ public final class KerberosRealm extends Realm implements CachingRealm { if (user != null) { listener.onResponse(AuthenticationResult.success(user)); } else { - buildUser(username, listener); + final String realmName = (userAndRealmName.length > 1) ? userAndRealmName[1] : null; + final Map metadata = new HashMap<>(); + metadata.put(KRB_METADATA_REALM_NAME_KEY, realmName); + metadata.put(KRB_METADATA_UPN_KEY, userPrincipalName); + buildUser(username, metadata, listener); } } } - private void buildUser(final String username, final ActionListener listener) { - final UserRoleMapper.UserData userData = new UserRoleMapper.UserData(username, null, Collections.emptySet(), null, this.config); + private void buildUser(final String username, final Map metadata, final ActionListener listener) { + final UserRoleMapper.UserData userData = new UserRoleMapper.UserData(username, null, Collections.emptySet(), metadata, this.config); userRoleMapper.resolveRoles(userData, ActionListener.wrap(roles -> { - final User computedUser = new User(username, roles.toArray(new String[roles.size()]), null, null, null, true); + final User computedUser = new User(username, roles.toArray(new String[roles.size()]), null, null, userData.getMetadata(), true); if (userPrincipalNameToUserCache != null) { userPrincipalNameToUserCache.put(username, computedUser); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmAuthenticateFailedTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmAuthenticateFailedTests.java index 7c5904d048a..dcb087ff147 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmAuthenticateFailedTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmAuthenticateFailedTests.java @@ -25,7 +25,9 @@ import org.ietf.jgss.GSSException; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import javax.security.auth.login.LoginException; @@ -86,7 +88,10 @@ public class KerberosRealmAuthenticateFailedTests extends KerberosRealmTestCase assertThat(result, is(notNullValue())); if (validTicket) { final String expectedUsername = maybeRemoveRealmName(username); - final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, null, true); + final Map metadata = new HashMap<>(); + metadata.put(KerberosRealm.KRB_METADATA_REALM_NAME_KEY, realmName(username)); + metadata.put(KerberosRealm.KRB_METADATA_UPN_KEY, username); + final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, metadata, true); assertSuccessAuthenticationResult(expectedUser, outToken, result); } else { assertThat(result.getStatus(), is(equalTo(AuthenticationResult.Status.TERMINATE))); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java index ee2e2675e18..2bef16883bb 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java @@ -18,7 +18,9 @@ import org.ietf.jgss.GSSException; import java.io.IOException; import java.nio.file.Path; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import javax.security.auth.login.LoginException; @@ -40,7 +42,10 @@ public class KerberosRealmCacheTests extends KerberosRealmTestCase { final KerberosRealm kerberosRealm = createKerberosRealm(username); final String expectedUsername = maybeRemoveRealmName(username); - final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, null, true); + final Map metadata = new HashMap<>(); + metadata.put(KerberosRealm.KRB_METADATA_REALM_NAME_KEY, realmName(username)); + metadata.put(KerberosRealm.KRB_METADATA_UPN_KEY, username); + final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, metadata, true); final byte[] decodedTicket = randomByteArrayOfLength(10); final Path keytabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings())); final boolean krbDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings()); @@ -72,7 +77,10 @@ public class KerberosRealmCacheTests extends KerberosRealmTestCase { final boolean krbDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings()); mockKerberosTicketValidator(decodedTicket, keytabPath, krbDebug, new Tuple<>(authNUsername, outToken), null); final String expectedUsername = maybeRemoveRealmName(authNUsername); - final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, null, true); + final Map metadata = new HashMap<>(); + metadata.put(KerberosRealm.KRB_METADATA_REALM_NAME_KEY, realmName(authNUsername)); + metadata.put(KerberosRealm.KRB_METADATA_UPN_KEY, authNUsername); + final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, metadata, true); final KerberosAuthenticationToken kerberosAuthenticationToken = new KerberosAuthenticationToken(decodedTicket); final User user1 = authenticateAndAssertResult(kerberosRealm, expectedUser, kerberosAuthenticationToken, outToken); @@ -110,7 +118,10 @@ public class KerberosRealmCacheTests extends KerberosRealmTestCase { final KerberosRealm kerberosRealm = createKerberosRealm(username); final String expectedUsername = maybeRemoveRealmName(username); - final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, null, true); + final Map metadata = new HashMap<>(); + metadata.put(KerberosRealm.KRB_METADATA_REALM_NAME_KEY, realmName(username)); + metadata.put(KerberosRealm.KRB_METADATA_UPN_KEY, username); + final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, metadata, true); final byte[] decodedTicket = randomByteArrayOfLength(10); final Path keytabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings())); final boolean krbDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings()); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java index 8f959a26bb8..4c0b77e320a 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java @@ -160,6 +160,7 @@ public abstract class KerberosRealmTestCase extends ESTestCase { if (withInstance) { principalName.append("/").append(randomAlphaOfLength(5)); } + principalName.append("@"); principalName.append(randomAlphaOfLength(5).toUpperCase(Locale.ROOT)); return principalName.toString(); } @@ -183,6 +184,19 @@ public abstract class KerberosRealmTestCase extends ESTestCase { return principalName; } + /** + * Extracts and returns realm part from the principal name. + * @param principalName user principal name + * @return realm name if found else returns {@code null} + */ + protected String realmName(final String principalName) { + String[] values = principalName.split("@"); + if (values.length > 1) { + return values[1]; + } + return null; + } + /** * Write content to provided keytab file. * diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTests.java index 1166e929341..3c7c3d3473f 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTests.java @@ -38,7 +38,9 @@ import java.nio.file.attribute.PosixFilePermissions; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; +import java.util.HashMap; import java.util.Locale; +import java.util.Map; import java.util.Set; import javax.security.auth.login.LoginException; @@ -71,7 +73,10 @@ public class KerberosRealmTests extends KerberosRealmTestCase { final String username = randomPrincipalName(); final KerberosRealm kerberosRealm = createKerberosRealm(username); final String expectedUsername = maybeRemoveRealmName(username); - final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, null, true); + final Map metadata = new HashMap<>(); + metadata.put(KerberosRealm.KRB_METADATA_REALM_NAME_KEY, realmName(username)); + metadata.put(KerberosRealm.KRB_METADATA_UPN_KEY, username); + final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, metadata, true); final byte[] decodedTicket = "base64encodedticket".getBytes(StandardCharsets.UTF_8); final Path keytabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings())); final boolean krbDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings()); From e9826164bddf0263f0a826e6d2824d5f103bb5b4 Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Fri, 14 Sep 2018 10:14:02 +0400 Subject: [PATCH 74/78] Mute FullClusterRestartSettingsUpgradeIT Tracked by #33697 --- .../upgrades/FullClusterRestartSettingsUpgradeIT.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartSettingsUpgradeIT.java b/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartSettingsUpgradeIT.java index 19fbdc92fae..5e3ccc75b74 100644 --- a/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartSettingsUpgradeIT.java +++ b/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartSettingsUpgradeIT.java @@ -19,6 +19,7 @@ package org.elasticsearch.upgrades; +import org.apache.lucene.util.LuceneTestCase.AwaitsFix; import org.elasticsearch.Version; import org.elasticsearch.action.admin.cluster.settings.ClusterGetSettingsResponse; import org.elasticsearch.client.Request; @@ -39,6 +40,7 @@ import static org.elasticsearch.transport.RemoteClusterAware.SEARCH_REMOTE_CLUST import static org.elasticsearch.transport.RemoteClusterService.SEARCH_REMOTE_CLUSTER_SKIP_UNAVAILABLE; import static org.hamcrest.Matchers.equalTo; +@AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/33697") public class FullClusterRestartSettingsUpgradeIT extends AbstractFullClusterRestartTestCase { public void testRemoteClusterSettingsUpgraded() throws IOException { From c9131983f53d91d6161a9a1e7747c70b28dc0455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BCscher?= Date: Fri, 14 Sep 2018 09:41:20 +0200 Subject: [PATCH 75/78] [Docs] Minor fix in `has_child` javadoc comment (#33674) The min and max constants are accidentaly the wrong way around. --- .../org/elasticsearch/join/query/HasChildQueryBuilder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/parent-join/src/main/java/org/elasticsearch/join/query/HasChildQueryBuilder.java b/modules/parent-join/src/main/java/org/elasticsearch/join/query/HasChildQueryBuilder.java index e37a7960091..696c4a72bdb 100644 --- a/modules/parent-join/src/main/java/org/elasticsearch/join/query/HasChildQueryBuilder.java +++ b/modules/parent-join/src/main/java/org/elasticsearch/join/query/HasChildQueryBuilder.java @@ -183,7 +183,7 @@ public class HasChildQueryBuilder extends AbstractQueryBuilder Date: Fri, 14 Sep 2018 09:59:06 +0200 Subject: [PATCH 76/78] [TEST] wait for no initializing shards --- .../java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java | 1 + .../src/test/java/org/elasticsearch/xpack/ccr/FollowIndexIT.java | 1 + 2 files changed, 2 insertions(+) diff --git a/x-pack/plugin/ccr/qa/multi-cluster-with-security/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java b/x-pack/plugin/ccr/qa/multi-cluster-with-security/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java index 43b16727aac..851a292ddae 100644 --- a/x-pack/plugin/ccr/qa/multi-cluster-with-security/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java +++ b/x-pack/plugin/ccr/qa/multi-cluster-with-security/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java @@ -243,6 +243,7 @@ public class FollowIndexSecurityIT extends ESRestTestCase { Request request = new Request("GET", "/_cluster/health/" + index); request.addParameter("wait_for_status", "yellow"); request.addParameter("wait_for_no_relocating_shards", "true"); + request.addParameter("wait_for_no_initializing_shards", "true"); request.addParameter("timeout", "70s"); request.addParameter("level", "shards"); adminClient().performRequest(request); diff --git a/x-pack/plugin/ccr/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexIT.java b/x-pack/plugin/ccr/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexIT.java index 5c1c3915044..c7ecbe184de 100644 --- a/x-pack/plugin/ccr/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexIT.java +++ b/x-pack/plugin/ccr/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexIT.java @@ -204,6 +204,7 @@ public class FollowIndexIT extends ESRestTestCase { Request request = new Request("GET", "/_cluster/health/" + index); request.addParameter("wait_for_status", "yellow"); request.addParameter("wait_for_no_relocating_shards", "true"); + request.addParameter("wait_for_no_initializing_shards", "true"); request.addParameter("timeout", "70s"); request.addParameter("level", "shards"); client().performRequest(request); From 5f495c18dfd7cc8896b5a84868a843c0602d50e3 Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Fri, 14 Sep 2018 10:22:11 +0200 Subject: [PATCH 77/78] Adapt skip version for doc_values format deprecation This commit fixes bwc rest tests for the doc_values format deprecation in search. The message of the deprecation changed in 6.4.1 so the bwc test should not check against 6.4.0. --- .../test/search/10_source_filtering.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/search/10_source_filtering.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/search/10_source_filtering.yml index 2725580d9e8..a5f50464794 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/search/10_source_filtering.yml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/search/10_source_filtering.yml @@ -134,8 +134,8 @@ setup: --- "docvalue_fields": - skip: - version: " - 6.3.99" - reason: format option was added in 6.4 + version: " - 6.4.0" + reason: format option was added in 6.4 and the deprecation message changed in 6.4.1 features: warnings - do: warnings: @@ -148,8 +148,8 @@ setup: --- "multiple docvalue_fields": - skip: - version: " - 6.3.99" - reason: format option was added in 6.4 + version: " - 6.4.0" + reason: format option was added in 6.4 and the deprecation message changed in 6.4.1 features: warnings - do: warnings: @@ -162,8 +162,8 @@ setup: --- "docvalue_fields as url param": - skip: - version: " - 6.3.99" - reason: format option was added in 6.4 + version: " - 6.4.0" + reason: format option was added in 6.4 and the deprecation message changed in 6.4.1 features: warnings - do: warnings: From 568ac10ca696588dd2c4ce9ee2b0895643e4cef0 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Fri, 14 Sep 2018 09:29:11 +0100 Subject: [PATCH 78/78] [ML] Allow overrides for some file structure detection decisions (#33630) This change modifies the file structure detection functionality such that some of the decisions can be overridden with user supplied values. The fields that can be overridden are: - charset - format - has_header_row - column_names - delimiter - quote - should_trim_fields - grok_pattern - timestamp_field - timestamp_format If an override makes finding the file structure impossible then the endpoint will return an exception. --- .../ml/action/FindFileStructureAction.java | 217 +++++++++++++- .../ml/filestructurefinder/FileStructure.java | 83 ++++-- .../FindFileStructureActionRequestTests.java | 94 +++++- .../FileStructureTests.java | 1 + .../TransportFindFileStructureAction.java | 5 +- .../DelimitedFileStructureFinder.java | 90 ++++-- .../DelimitedFileStructureFinderFactory.java | 22 +- .../FileStructureFinderFactory.java | 18 +- .../FileStructureFinderManager.java | 85 +++++- .../FileStructureOverrides.java | 205 +++++++++++++ .../FileStructureUtils.java | 59 +++- .../GrokPatternCreator.java | 114 ++++--- .../JsonFileStructureFinder.java | 10 +- .../JsonFileStructureFinderFactory.java | 12 +- .../TextLogFileStructureFinder.java | 41 ++- .../TextLogFileStructureFinderFactory.java | 13 +- .../TimestampFormatFinder.java | 88 ++++-- .../XmlFileStructureFinder.java | 10 +- .../XmlFileStructureFinderFactory.java | 11 +- .../ml/rest/RestFindFileStructureAction.java | 11 + ...imitedFileStructureFinderFactoryTests.java | 8 +- .../DelimitedFileStructureFinderTests.java | 202 ++++++++++++- .../FileStructureFinderManagerTests.java | 62 +++- .../FileStructureUtilsTests.java | 94 ++++-- .../GrokPatternCreatorTests.java | 61 +++- .../JsonFileStructureFinderTests.java | 4 +- .../TextLogFileStructureFinderTests.java | 277 ++++++++++++------ .../XmlFileStructureFinderTests.java | 4 +- .../api/xpack.ml.find_file_structure.json | 43 ++- .../test/ml/find_file_structure.yml | 55 +++- 30 files changed, 1667 insertions(+), 332 deletions(-) create mode 100644 x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureOverrides.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/FindFileStructureAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/FindFileStructureAction.java index 9fda416b33b..d10fedfb589 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/FindFileStructureAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/FindFileStructureAction.java @@ -22,6 +22,9 @@ import org.elasticsearch.rest.RestStatus; import org.elasticsearch.xpack.core.ml.filestructurefinder.FileStructure; import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; import java.util.Objects; import static org.elasticsearch.action.ValidateActions.addValidationError; @@ -109,8 +112,32 @@ public class FindFileStructureAction extends Action columnNames; + private Boolean hasHeaderRow; + private Character delimiter; + private Character quote; + private Boolean shouldTrimFields; + private String grokPattern; + private String timestampFormat; + private String timestampField; private BytesReference sample; public Request() { @@ -124,6 +151,114 @@ public class FindFileStructureAction extends Action getColumnNames() { + return columnNames; + } + + public void setColumnNames(List columnNames) { + this.columnNames = (columnNames == null || columnNames.isEmpty()) ? null : columnNames; + } + + public void setColumnNames(String[] columnNames) { + this.columnNames = (columnNames == null || columnNames.length == 0) ? null : Arrays.asList(columnNames); + } + + public Boolean getHasHeaderRow() { + return hasHeaderRow; + } + + public void setHasHeaderRow(Boolean hasHeaderRow) { + this.hasHeaderRow = hasHeaderRow; + } + + public Character getDelimiter() { + return delimiter; + } + + public void setDelimiter(Character delimiter) { + this.delimiter = delimiter; + } + + public void setDelimiter(String delimiter) { + if (delimiter == null || delimiter.isEmpty()) { + this.delimiter = null; + } else if (delimiter.length() == 1) { + this.delimiter = delimiter.charAt(0); + } else { + throw new IllegalArgumentException(DELIMITER.getPreferredName() + " must be a single character"); + } + } + + public Character getQuote() { + return quote; + } + + public void setQuote(Character quote) { + this.quote = quote; + } + + public void setQuote(String quote) { + if (quote == null || quote.isEmpty()) { + this.quote = null; + } else if (quote.length() == 1) { + this.quote = quote.charAt(0); + } else { + throw new IllegalArgumentException(QUOTE.getPreferredName() + " must be a single character"); + } + } + + public Boolean getShouldTrimFields() { + return shouldTrimFields; + } + + public void setShouldTrimFields(Boolean shouldTrimFields) { + this.shouldTrimFields = shouldTrimFields; + } + + public String getGrokPattern() { + return grokPattern; + } + + public void setGrokPattern(String grokPattern) { + this.grokPattern = (grokPattern == null || grokPattern.isEmpty()) ? null : grokPattern; + } + + public String getTimestampFormat() { + return timestampFormat; + } + + public void setTimestampFormat(String timestampFormat) { + this.timestampFormat = (timestampFormat == null || timestampFormat.isEmpty()) ? null : timestampFormat; + } + + public String getTimestampField() { + return timestampField; + } + + public void setTimestampField(String timestampField) { + this.timestampField = (timestampField == null || timestampField.isEmpty()) ? null : timestampField; + } + public BytesReference getSample() { return sample; } @@ -132,12 +267,41 @@ public class FindFileStructureAction extends Action PARSER = new ObjectParser<>("file_structure", false, Builder::new); @@ -112,12 +113,13 @@ public class FileStructure implements ToXContentObject, Writeable { PARSER.declareString(Builder::setSampleStart, SAMPLE_START); PARSER.declareString(Builder::setCharset, CHARSET); PARSER.declareBoolean(Builder::setHasByteOrderMarker, HAS_BYTE_ORDER_MARKER); - PARSER.declareString((p, c) -> p.setFormat(Format.fromString(c)), STRUCTURE); + PARSER.declareString((p, c) -> p.setFormat(Format.fromString(c)), FORMAT); PARSER.declareString(Builder::setMultilineStartPattern, MULTILINE_START_PATTERN); PARSER.declareString(Builder::setExcludeLinesPattern, EXCLUDE_LINES_PATTERN); PARSER.declareStringArray(Builder::setColumnNames, COLUMN_NAMES); PARSER.declareBoolean(Builder::setHasHeaderRow, HAS_HEADER_ROW); PARSER.declareString((p, c) -> p.setDelimiter(c.charAt(0)), DELIMITER); + PARSER.declareString((p, c) -> p.setQuote(c.charAt(0)), QUOTE); PARSER.declareBoolean(Builder::setShouldTrimFields, SHOULD_TRIM_FIELDS); PARSER.declareString(Builder::setGrokPattern, GROK_PATTERN); PARSER.declareString(Builder::setTimestampField, TIMESTAMP_FIELD); @@ -145,6 +147,7 @@ public class FileStructure implements ToXContentObject, Writeable { private final List columnNames; private final Boolean hasHeaderRow; private final Character delimiter; + private final Character quote; private final Boolean shouldTrimFields; private final String grokPattern; private final List timestampFormats; @@ -156,8 +159,8 @@ public class FileStructure implements ToXContentObject, Writeable { public FileStructure(int numLinesAnalyzed, int numMessagesAnalyzed, String sampleStart, String charset, Boolean hasByteOrderMarker, Format format, String multilineStartPattern, String excludeLinesPattern, List columnNames, - Boolean hasHeaderRow, Character delimiter, Boolean shouldTrimFields, String grokPattern, String timestampField, - List timestampFormats, boolean needClientTimezone, Map mappings, + Boolean hasHeaderRow, Character delimiter, Character quote, Boolean shouldTrimFields, String grokPattern, + String timestampField, List timestampFormats, boolean needClientTimezone, Map mappings, Map fieldStats, List explanation) { this.numLinesAnalyzed = numLinesAnalyzed; @@ -171,6 +174,7 @@ public class FileStructure implements ToXContentObject, Writeable { this.columnNames = (columnNames == null) ? null : Collections.unmodifiableList(new ArrayList<>(columnNames)); this.hasHeaderRow = hasHeaderRow; this.delimiter = delimiter; + this.quote = quote; this.shouldTrimFields = shouldTrimFields; this.grokPattern = grokPattern; this.timestampField = timestampField; @@ -193,6 +197,7 @@ public class FileStructure implements ToXContentObject, Writeable { columnNames = in.readBoolean() ? Collections.unmodifiableList(in.readList(StreamInput::readString)) : null; hasHeaderRow = in.readOptionalBoolean(); delimiter = in.readBoolean() ? (char) in.readVInt() : null; + quote = in.readBoolean() ? (char) in.readVInt() : null; shouldTrimFields = in.readOptionalBoolean(); grokPattern = in.readOptionalString(); timestampFormats = in.readBoolean() ? Collections.unmodifiableList(in.readList(StreamInput::readString)) : null; @@ -226,6 +231,12 @@ public class FileStructure implements ToXContentObject, Writeable { out.writeBoolean(true); out.writeVInt(delimiter); } + if (quote == null) { + out.writeBoolean(false); + } else { + out.writeBoolean(true); + out.writeVInt(quote); + } out.writeOptionalBoolean(shouldTrimFields); out.writeOptionalString(grokPattern); if (timestampFormats == null) { @@ -285,6 +296,10 @@ public class FileStructure implements ToXContentObject, Writeable { return delimiter; } + public Character getQuote() { + return quote; + } + public Boolean getShouldTrimFields() { return shouldTrimFields; } @@ -328,7 +343,7 @@ public class FileStructure implements ToXContentObject, Writeable { if (hasByteOrderMarker != null) { builder.field(HAS_BYTE_ORDER_MARKER.getPreferredName(), hasByteOrderMarker.booleanValue()); } - builder.field(STRUCTURE.getPreferredName(), format); + builder.field(FORMAT.getPreferredName(), format); if (multilineStartPattern != null && multilineStartPattern.isEmpty() == false) { builder.field(MULTILINE_START_PATTERN.getPreferredName(), multilineStartPattern); } @@ -344,6 +359,9 @@ public class FileStructure implements ToXContentObject, Writeable { if (delimiter != null) { builder.field(DELIMITER.getPreferredName(), String.valueOf(delimiter)); } + if (quote != null) { + builder.field(QUOTE.getPreferredName(), String.valueOf(quote)); + } if (shouldTrimFields != null) { builder.field(SHOULD_TRIM_FIELDS.getPreferredName(), shouldTrimFields.booleanValue()); } @@ -377,8 +395,8 @@ public class FileStructure implements ToXContentObject, Writeable { public int hashCode() { return Objects.hash(numLinesAnalyzed, numMessagesAnalyzed, sampleStart, charset, hasByteOrderMarker, format, - multilineStartPattern, excludeLinesPattern, columnNames, hasHeaderRow, delimiter, shouldTrimFields, grokPattern, timestampField, - timestampFormats, needClientTimezone, mappings, fieldStats, explanation); + multilineStartPattern, excludeLinesPattern, columnNames, hasHeaderRow, delimiter, quote, shouldTrimFields, grokPattern, + timestampField, timestampFormats, needClientTimezone, mappings, fieldStats, explanation); } @Override @@ -405,6 +423,7 @@ public class FileStructure implements ToXContentObject, Writeable { Objects.equals(this.columnNames, that.columnNames) && Objects.equals(this.hasHeaderRow, that.hasHeaderRow) && Objects.equals(this.delimiter, that.delimiter) && + Objects.equals(this.quote, that.quote) && Objects.equals(this.shouldTrimFields, that.shouldTrimFields) && Objects.equals(this.grokPattern, that.grokPattern) && Objects.equals(this.timestampField, that.timestampField) && @@ -427,6 +446,7 @@ public class FileStructure implements ToXContentObject, Writeable { private List columnNames; private Boolean hasHeaderRow; private Character delimiter; + private Character quote; private Boolean shouldTrimFields; private String grokPattern; private String timestampField; @@ -499,6 +519,11 @@ public class FileStructure implements ToXContentObject, Writeable { return this; } + public Builder setQuote(Character quote) { + this.quote = quote; + return this; + } + public Builder setShouldTrimFields(Boolean shouldTrimFields) { this.shouldTrimFields = shouldTrimFields; return this; @@ -582,6 +607,9 @@ public class FileStructure implements ToXContentObject, Writeable { if (delimiter != null) { throw new IllegalArgumentException("Delimiter may not be specified for [" + format + "] structures."); } + if (quote != null) { + throw new IllegalArgumentException("Quote may not be specified for [" + format + "] structures."); + } if (grokPattern != null) { throw new IllegalArgumentException("Grok pattern may not be specified for [" + format + "] structures."); } @@ -610,6 +638,9 @@ public class FileStructure implements ToXContentObject, Writeable { if (delimiter != null) { throw new IllegalArgumentException("Delimiter may not be specified for [" + format + "] structures."); } + if (quote != null) { + throw new IllegalArgumentException("Quote may not be specified for [" + format + "] structures."); + } if (shouldTrimFields != null) { throw new IllegalArgumentException("Should trim fields may not be specified for [" + format + "] structures."); } @@ -638,7 +669,7 @@ public class FileStructure implements ToXContentObject, Writeable { } return new FileStructure(numLinesAnalyzed, numMessagesAnalyzed, sampleStart, charset, hasByteOrderMarker, format, - multilineStartPattern, excludeLinesPattern, columnNames, hasHeaderRow, delimiter, shouldTrimFields, grokPattern, + multilineStartPattern, excludeLinesPattern, columnNames, hasHeaderRow, delimiter, quote, shouldTrimFields, grokPattern, timestampField, timestampFormats, needClientTimezone, mappings, fieldStats, explanation); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/FindFileStructureActionRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/FindFileStructureActionRequestTests.java index 05ba0e7f306..21f11fa5f73 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/FindFileStructureActionRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/FindFileStructureActionRequestTests.java @@ -8,6 +8,9 @@ package org.elasticsearch.xpack.core.ml.action; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.test.AbstractStreamableTestCase; +import org.elasticsearch.xpack.core.ml.filestructurefinder.FileStructure; + +import java.util.Arrays; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.startsWith; @@ -22,6 +25,44 @@ public class FindFileStructureActionRequestTests extends AbstractStreamableTestC if (randomBoolean()) { request.setLinesToSample(randomIntBetween(10, 2000)); } + + if (randomBoolean()) { + request.setCharset(randomAlphaOfLength(10)); + } + + if (randomBoolean()) { + FileStructure.Format format = randomFrom(FileStructure.Format.values()); + request.setFormat(format); + if (format == FileStructure.Format.DELIMITED) { + if (randomBoolean()) { + request.setColumnNames(generateRandomStringArray(10, 15, false, false)); + } + if (randomBoolean()) { + request.setHasHeaderRow(randomBoolean()); + } + if (randomBoolean()) { + request.setDelimiter(randomFrom(',', '\t', ';', '|')); + } + if (randomBoolean()) { + request.setQuote(randomFrom('"', '\'')); + } + if (randomBoolean()) { + request.setShouldTrimFields(randomBoolean()); + } + } else if (format == FileStructure.Format.SEMI_STRUCTURED_TEXT) { + if (randomBoolean()) { + request.setGrokPattern(randomAlphaOfLength(80)); + } + } + } + + if (randomBoolean()) { + request.setTimestampFormat(randomAlphaOfLength(20)); + } + if (randomBoolean()) { + request.setTimestampField(randomAlphaOfLength(15)); + } + request.setSample(new BytesArray(randomByteArrayOfLength(randomIntBetween(1000, 20000)))); return request; @@ -35,13 +76,62 @@ public class FindFileStructureActionRequestTests extends AbstractStreamableTestC public void testValidateLinesToSample() { FindFileStructureAction.Request request = new FindFileStructureAction.Request(); - request.setLinesToSample(randomFrom(-1, 0)); + request.setLinesToSample(randomIntBetween(-1, 0)); request.setSample(new BytesArray("foo\n")); ActionRequestValidationException e = request.validate(); assertNotNull(e); assertThat(e.getMessage(), startsWith("Validation Failed: ")); - assertThat(e.getMessage(), containsString(" lines_to_sample must be positive if specified")); + assertThat(e.getMessage(), containsString(" [lines_to_sample] must be positive if specified")); + } + + public void testValidateNonDelimited() { + + FindFileStructureAction.Request request = new FindFileStructureAction.Request(); + String errorField; + switch (randomIntBetween(0, 4)) { + case 0: + errorField = "column_names"; + request.setColumnNames(Arrays.asList("col1", "col2")); + break; + case 1: + errorField = "has_header_row"; + request.setHasHeaderRow(randomBoolean()); + break; + case 2: + errorField = "delimiter"; + request.setDelimiter(randomFrom(',', '\t', ';', '|')); + break; + case 3: + errorField = "quote"; + request.setQuote(randomFrom('"', '\'')); + break; + case 4: + errorField = "should_trim_fields"; + request.setShouldTrimFields(randomBoolean()); + break; + default: + throw new IllegalStateException("unexpected switch value"); + } + request.setSample(new BytesArray("foo\n")); + + ActionRequestValidationException e = request.validate(); + assertNotNull(e); + assertThat(e.getMessage(), startsWith("Validation Failed: ")); + assertThat(e.getMessage(), containsString(" [" + errorField + "] may only be specified if [format] is [delimited]")); + } + + public void testValidateNonSemiStructuredText() { + + FindFileStructureAction.Request request = new FindFileStructureAction.Request(); + request.setFormat(randomFrom(FileStructure.Format.JSON, FileStructure.Format.XML, FileStructure.Format.DELIMITED)); + request.setGrokPattern(randomAlphaOfLength(80)); + request.setSample(new BytesArray("foo\n")); + + ActionRequestValidationException e = request.validate(); + assertNotNull(e); + assertThat(e.getMessage(), startsWith("Validation Failed: ")); + assertThat(e.getMessage(), containsString(" [grok_pattern] may only be specified if [format] is [semi_structured_text]")); } public void testValidateSample() { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/filestructurefinder/FileStructureTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/filestructurefinder/FileStructureTests.java index e09b9e3f91e..ac6c647136b 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/filestructurefinder/FileStructureTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/filestructurefinder/FileStructureTests.java @@ -54,6 +54,7 @@ public class FileStructureTests extends AbstractSerializingTestCase { @@ -49,8 +50,8 @@ public class TransportFindFileStructureAction FileStructureFinderManager structureFinderManager = new FileStructureFinderManager(); - FileStructureFinder fileStructureFinder = - structureFinderManager.findFileStructure(request.getLinesToSample(), request.getSample().streamInput()); + FileStructureFinder fileStructureFinder = structureFinderManager.findFileStructure(request.getLinesToSample(), + request.getSample().streamInput(), new FileStructureOverrides(request)); return new FindFileStructureAction.Response(fileStructureFinder.getStructure()); } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinder.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinder.java index ba6b590dfc8..a103560480d 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinder.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinder.java @@ -33,6 +33,7 @@ import java.util.stream.IntStream; public class DelimitedFileStructureFinder implements FileStructureFinder { + private static final String REGEX_NEEDS_ESCAPE_PATTERN = "([\\\\|()\\[\\]{}^$.+*?])"; private static final int MAX_LEVENSHTEIN_COMPARISONS = 100; private final List sampleMessages; @@ -40,21 +41,35 @@ public class DelimitedFileStructureFinder implements FileStructureFinder { static DelimitedFileStructureFinder makeDelimitedFileStructureFinder(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker, CsvPreference csvPreference, - boolean trimFields) throws IOException { + boolean trimFields, FileStructureOverrides overrides) + throws IOException { Tuple>, List> parsed = readRows(sample, csvPreference); List> rows = parsed.v1(); List lineNumbers = parsed.v2(); - Tuple headerInfo = findHeaderFromSample(explanation, rows); + // Even if the column names are overridden we need to know if there's a + // header in the file, as it affects which rows are considered records + Tuple headerInfo = findHeaderFromSample(explanation, rows, overrides); boolean isHeaderInFile = headerInfo.v1(); String[] header = headerInfo.v2(); - // The column names are the header names but with blanks named column1, column2, etc. - String[] columnNames = new String[header.length]; - for (int i = 0; i < header.length; ++i) { - assert header[i] != null; - String rawHeader = trimFields ? header[i].trim() : header[i]; - columnNames[i] = rawHeader.isEmpty() ? "column" + (i + 1) : rawHeader; + + String[] columnNames; + List overriddenColumnNames = overrides.getColumnNames(); + if (overriddenColumnNames != null) { + if (overriddenColumnNames.size() != header.length) { + throw new IllegalArgumentException("[" + overriddenColumnNames.size() + "] column names were specified [" + + String.join(",", overriddenColumnNames) + "] but there are [" + header.length + "] columns in the sample"); + } + columnNames = overriddenColumnNames.toArray(new String[overriddenColumnNames.size()]); + } else { + // The column names are the header names but with blanks named column1, column2, etc. + columnNames = new String[header.length]; + for (int i = 0; i < header.length; ++i) { + assert header[i] != null; + String rawHeader = trimFields ? header[i].trim() : header[i]; + columnNames[i] = rawHeader.isEmpty() ? "column" + (i + 1) : rawHeader; + } } List sampleLines = Arrays.asList(sample.split("\n")); @@ -84,13 +99,14 @@ public class DelimitedFileStructureFinder implements FileStructureFinder { .setNumMessagesAnalyzed(sampleRecords.size()) .setHasHeaderRow(isHeaderInFile) .setDelimiter(delimiter) + .setQuote(csvPreference.getQuoteChar()) .setColumnNames(Arrays.stream(columnNames).collect(Collectors.toList())); if (trimFields) { structureBuilder.setShouldTrimFields(true); } - Tuple timeField = FileStructureUtils.guessTimestampField(explanation, sampleRecords); + Tuple timeField = FileStructureUtils.guessTimestampField(explanation, sampleRecords, overrides); if (timeField != null) { String timeLineRegex = null; StringBuilder builder = new StringBuilder("^"); @@ -98,7 +114,7 @@ public class DelimitedFileStructureFinder implements FileStructureFinder { // timestamp is the last column then either our assumption is wrong (and the approach will completely // break down) or else every record is on a single line and there's no point creating a multiline config. // This is why the loop excludes the last column. - for (String column : Arrays.asList(header).subList(0, header.length - 1)) { + for (String column : Arrays.asList(columnNames).subList(0, columnNames.length - 1)) { if (timeField.v1().equals(column)) { builder.append("\"?"); String simpleTimePattern = timeField.v2().simplePattern.pattern(); @@ -116,8 +132,11 @@ public class DelimitedFileStructureFinder implements FileStructureFinder { } if (isHeaderInFile) { + String quote = String.valueOf(csvPreference.getQuoteChar()); + String twoQuotes = quote + quote; + String optQuote = quote.replaceAll(REGEX_NEEDS_ESCAPE_PATTERN, "\\\\$1") + "?"; structureBuilder.setExcludeLinesPattern("^" + Arrays.stream(header) - .map(column -> "\"?" + column.replace("\"", "\"\"").replaceAll("([\\\\|()\\[\\]{}^$*?])", "\\\\$1") + "\"?") + .map(column -> optQuote + column.replace(quote, twoQuotes).replaceAll(REGEX_NEEDS_ESCAPE_PATTERN, "\\\\$1") + optQuote) .collect(Collectors.joining(","))); } @@ -131,7 +150,10 @@ public class DelimitedFileStructureFinder implements FileStructureFinder { FileStructureUtils.guessMappingsAndCalculateFieldStats(explanation, sampleRecords); SortedMap mappings = mappingsAndFieldStats.v1(); - mappings.put(FileStructureUtils.DEFAULT_TIMESTAMP_FIELD, Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "date")); + if (timeField != null) { + mappings.put(FileStructureUtils.DEFAULT_TIMESTAMP_FIELD, + Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "date")); + } if (mappingsAndFieldStats.v2() != null) { structureBuilder.setFieldStats(mappingsAndFieldStats.v2()); @@ -205,45 +227,61 @@ public class DelimitedFileStructureFinder implements FileStructureFinder { return new Tuple<>(rows, lineNumbers); } - static Tuple findHeaderFromSample(List explanation, List> rows) { + static Tuple findHeaderFromSample(List explanation, List> rows, + FileStructureOverrides overrides) { assert rows.isEmpty() == false; + List overriddenColumnNames = overrides.getColumnNames(); List firstRow = rows.get(0); boolean isHeaderInFile = true; - if (rowContainsDuplicateNonEmptyValues(firstRow)) { - isHeaderInFile = false; - explanation.add("First row contains duplicate values, so assuming it's not a header"); + if (overrides.getHasHeaderRow() != null) { + isHeaderInFile = overrides.getHasHeaderRow(); + if (isHeaderInFile && overriddenColumnNames == null) { + String duplicateValue = findDuplicateNonEmptyValues(firstRow); + if (duplicateValue != null) { + throw new IllegalArgumentException("Sample specified to contain a header row, " + + "but the first row contains duplicate values: [" + duplicateValue + "]"); + } + } + explanation.add("Sample specified to " + (isHeaderInFile ? "contain" : "not contain") + " a header row"); } else { - if (rows.size() < 3) { - explanation.add("Too little data to accurately assess whether header is in sample - guessing it is"); + if (findDuplicateNonEmptyValues(firstRow) != null) { + isHeaderInFile = false; + explanation.add("First row contains duplicate values, so assuming it's not a header"); } else { - isHeaderInFile = isFirstRowUnusual(explanation, rows); + if (rows.size() < 3) { + explanation.add("Too little data to accurately assess whether header is in sample - guessing it is"); + } else { + isHeaderInFile = isFirstRowUnusual(explanation, rows); + } } } + String[] header; if (isHeaderInFile) { // SuperCSV will put nulls in the header if any columns don't have names, but empty strings are better for us - return new Tuple<>(true, firstRow.stream().map(field -> (field == null) ? "" : field).toArray(String[]::new)); + header = firstRow.stream().map(field -> (field == null) ? "" : field).toArray(String[]::new); } else { - String[] dummyHeader = new String[firstRow.size()]; - Arrays.fill(dummyHeader, ""); - return new Tuple<>(false, dummyHeader); + header = new String[firstRow.size()]; + Arrays.fill(header, ""); } + + return new Tuple<>(isHeaderInFile, header); } - static boolean rowContainsDuplicateNonEmptyValues(List row) { + static String findDuplicateNonEmptyValues(List row) { HashSet values = new HashSet<>(); for (String value : row) { if (value != null && value.isEmpty() == false && values.add(value) == false) { - return true; + return value; } } - return false; + return null; } private static boolean isFirstRowUnusual(List explanation, List> rows) { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinderFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinderFactory.java index 0bbe13e3b05..62e5eff517e 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinderFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinderFactory.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.ml.filestructurefinder; +import org.elasticsearch.xpack.core.ml.filestructurefinder.FileStructure; import org.supercsv.prefs.CsvPreference; import java.io.IOException; @@ -17,12 +18,23 @@ public class DelimitedFileStructureFinderFactory implements FileStructureFinderF private final int minFieldsPerRow; private final boolean trimFields; - DelimitedFileStructureFinderFactory(char delimiter, int minFieldsPerRow, boolean trimFields) { - csvPreference = new CsvPreference.Builder('"', delimiter, "\n").build(); + DelimitedFileStructureFinderFactory(char delimiter, char quote, int minFieldsPerRow, boolean trimFields) { + csvPreference = new CsvPreference.Builder(quote, delimiter, "\n").build(); this.minFieldsPerRow = minFieldsPerRow; this.trimFields = trimFields; } + DelimitedFileStructureFinderFactory makeSimilar(Character quote, Boolean trimFields) { + + return new DelimitedFileStructureFinderFactory((char) csvPreference.getDelimiterChar(), + (quote == null) ? csvPreference.getQuoteChar() : quote, minFieldsPerRow, (trimFields == null) ? this.trimFields : trimFields); + } + + @Override + public boolean canFindFormat(FileStructure.Format format) { + return format == null || format == FileStructure.Format.DELIMITED; + } + /** * Rules are: * - It must contain at least two complete records @@ -49,9 +61,9 @@ public class DelimitedFileStructureFinderFactory implements FileStructureFinderF } @Override - public FileStructureFinder createFromSample(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker) - throws IOException { + public FileStructureFinder createFromSample(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker, + FileStructureOverrides overrides) throws IOException { return DelimitedFileStructureFinder.makeDelimitedFileStructureFinder(explanation, sample, charsetName, hasByteOrderMarker, - csvPreference, trimFields); + csvPreference, trimFields, overrides); } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureFinderFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureFinderFactory.java index 4b6fce322ee..bff4b2115b0 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureFinderFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureFinderFactory.java @@ -5,10 +5,20 @@ */ package org.elasticsearch.xpack.ml.filestructurefinder; +import org.elasticsearch.xpack.core.ml.filestructurefinder.FileStructure; + import java.util.List; public interface FileStructureFinderFactory { + /** + * Can this factory create a {@link FileStructureFinder} that can find the supplied format? + * @param format The format to query, or null. + * @return true if {@code format} is null or the factory + * can produce a {@link FileStructureFinder} that can find {@code format}. + */ + boolean canFindFormat(FileStructure.Format format); + /** * Given a sample of a file, decide whether this factory will be able * to create an appropriate object to represent its ingestion configs. @@ -27,9 +37,11 @@ public interface FileStructureFinderFactory { * @param sample A sample from the file to be ingested. * @param charsetName The name of the character set in which the sample was provided. * @param hasByteOrderMarker Did the sample have a byte order marker? null means "not relevant". - * @return A file structure object suitable for ingesting the supplied sample. + * @param overrides Stores structure decisions that have been made by the end user, and should + * take precedence over anything the {@link FileStructureFinder} may decide. + * @return A {@link FileStructureFinder} object suitable for determining the structure of the supplied sample. * @throws Exception if something goes wrong during creation. */ - FileStructureFinder createFromSample(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker) - throws Exception; + FileStructureFinder createFromSample(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker, + FileStructureOverrides overrides) throws Exception; } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureFinderManager.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureFinderManager.java index d0ce68aff25..7949998d16e 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureFinderManager.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureFinderManager.java @@ -13,6 +13,7 @@ import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -24,6 +25,7 @@ import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; /** * Runs the high-level steps needed to create ingest configs for the specified file. In order: @@ -70,15 +72,19 @@ public final class FileStructureFinderManager { new JsonFileStructureFinderFactory(), new XmlFileStructureFinderFactory(), // ND-JSON will often also be valid (although utterly weird) CSV, so JSON must come before CSV - new DelimitedFileStructureFinderFactory(',', 2, false), - new DelimitedFileStructureFinderFactory('\t', 2, false), - new DelimitedFileStructureFinderFactory(';', 4, false), - new DelimitedFileStructureFinderFactory('|', 5, true), + new DelimitedFileStructureFinderFactory(',', '"', 2, false), + new DelimitedFileStructureFinderFactory('\t', '"', 2, false), + new DelimitedFileStructureFinderFactory(';', '"', 4, false), + new DelimitedFileStructureFinderFactory('|', '"', 5, true), new TextLogFileStructureFinderFactory() )); private static final int BUFFER_SIZE = 8192; + public FileStructureFinder findFileStructure(Integer idealSampleLineCount, InputStream fromFile) throws Exception { + return findFileStructure(idealSampleLineCount, fromFile, FileStructureOverrides.EMPTY_OVERRIDES); + } + /** * Given a stream of data from some file, determine its structure. * @param idealSampleLineCount Ideally, how many lines from the stream will be read to determine the structure? @@ -86,24 +92,42 @@ public final class FileStructureFinderManager { * least {@link #MIN_SAMPLE_LINE_COUNT} lines can be read. If null * the value of {@link #DEFAULT_IDEAL_SAMPLE_LINE_COUNT} will be used. * @param fromFile A stream from which the sample will be read. + * @param overrides Aspects of the file structure that are known in advance. These take precedence over + * values determined by structure analysis. An exception will be thrown if the file structure + * is incompatible with an overridden value. * @return A {@link FileStructureFinder} object from which the structure and messages can be queried. * @throws Exception A variety of problems could occur at various stages of the structure finding process. */ - public FileStructureFinder findFileStructure(Integer idealSampleLineCount, InputStream fromFile) throws Exception { + public FileStructureFinder findFileStructure(Integer idealSampleLineCount, InputStream fromFile, FileStructureOverrides overrides) + throws Exception { return findFileStructure(new ArrayList<>(), (idealSampleLineCount == null) ? DEFAULT_IDEAL_SAMPLE_LINE_COUNT : idealSampleLineCount, - fromFile); + fromFile, overrides); } public FileStructureFinder findFileStructure(List explanation, int idealSampleLineCount, InputStream fromFile) throws Exception { + return findFileStructure(new ArrayList<>(), idealSampleLineCount, fromFile, FileStructureOverrides.EMPTY_OVERRIDES); + } - CharsetMatch charsetMatch = findCharset(explanation, fromFile); - String charsetName = charsetMatch.getName(); + public FileStructureFinder findFileStructure(List explanation, int idealSampleLineCount, InputStream fromFile, + FileStructureOverrides overrides) throws Exception { - Tuple sampleInfo = sampleFile(charsetMatch.getReader(), charsetName, MIN_SAMPLE_LINE_COUNT, + String charsetName = overrides.getCharset(); + Reader sampleReader; + if (charsetName != null) { + // Creating the reader will throw if the specified character set does not exist + sampleReader = new InputStreamReader(fromFile, charsetName); + explanation.add("Using specified character encoding [" + charsetName + "]"); + } else { + CharsetMatch charsetMatch = findCharset(explanation, fromFile); + charsetName = charsetMatch.getName(); + sampleReader = charsetMatch.getReader(); + } + + Tuple sampleInfo = sampleFile(sampleReader, charsetName, MIN_SAMPLE_LINE_COUNT, Math.max(MIN_SAMPLE_LINE_COUNT, idealSampleLineCount)); - return makeBestStructureFinder(explanation, sampleInfo.v1(), charsetName, sampleInfo.v2()); + return makeBestStructureFinder(explanation, sampleInfo.v1(), charsetName, sampleInfo.v2(), overrides); } CharsetMatch findCharset(List explanation, InputStream inputStream) throws Exception { @@ -195,15 +219,44 @@ public final class FileStructureFinderManager { (containsZeroBytes ? " - could it be binary data?" : "")); } - FileStructureFinder makeBestStructureFinder(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker) - throws Exception { + FileStructureFinder makeBestStructureFinder(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker, + FileStructureOverrides overrides) throws Exception { - for (FileStructureFinderFactory factory : ORDERED_STRUCTURE_FACTORIES) { + Character delimiter = overrides.getDelimiter(); + Character quote = overrides.getQuote(); + Boolean shouldTrimFields = overrides.getShouldTrimFields(); + List factories; + if (delimiter != null) { + + // If a precise delimiter is specified, we only need one structure finder + // factory, and we'll tolerate as little as one column in the input + factories = Collections.singletonList(new DelimitedFileStructureFinderFactory(delimiter, (quote == null) ? '"' : quote, 1, + (shouldTrimFields == null) ? (delimiter == '|') : shouldTrimFields)); + + } else if (quote != null || shouldTrimFields != null) { + + // The delimiter is not specified, but some other aspect of delimited files is, + // so clone our default delimited factories altering the overridden values + factories = ORDERED_STRUCTURE_FACTORIES.stream().filter(factory -> factory instanceof DelimitedFileStructureFinderFactory) + .map(factory -> ((DelimitedFileStructureFinderFactory) factory).makeSimilar(quote, shouldTrimFields)) + .collect(Collectors.toList()); + + } else { + + // We can use the default factories, but possibly filtered down to a specific format + factories = ORDERED_STRUCTURE_FACTORIES.stream() + .filter(factory -> factory.canFindFormat(overrides.getFormat())).collect(Collectors.toList()); + + } + + for (FileStructureFinderFactory factory : factories) { if (factory.canCreateFromSample(explanation, sample)) { - return factory.createFromSample(explanation, sample, charsetName, hasByteOrderMarker); + return factory.createFromSample(explanation, sample, charsetName, hasByteOrderMarker, overrides); } } - throw new IllegalArgumentException("Input did not match any known formats"); + + throw new IllegalArgumentException("Input did not match " + + ((overrides.getFormat() == null) ? "any known formats" : "the specified format [" + overrides.getFormat() + "]")); } private Tuple sampleFile(Reader reader, String charsetName, int minLines, int maxLines) throws IOException { @@ -233,7 +286,7 @@ public final class FileStructureFinderManager { } if (lineCount < minLines) { - throw new IllegalArgumentException("Input contained too few lines to sample"); + throw new IllegalArgumentException("Input contained too few lines [" + lineCount + "] to obtain a meaningful sample"); } return new Tuple<>(sample.toString(), hasByteOrderMarker); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureOverrides.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureOverrides.java new file mode 100644 index 00000000000..e30699c69b7 --- /dev/null +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureOverrides.java @@ -0,0 +1,205 @@ +/* + * 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.filestructurefinder; + +import org.elasticsearch.xpack.core.ml.action.FindFileStructureAction; +import org.elasticsearch.xpack.core.ml.filestructurefinder.FileStructure; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * An immutable holder for the aspects of file structure detection that can be overridden + * by the end user. Every field can be null, and this means that that + * aspect of the file structure detection is not overridden. + * + * There is no consistency checking in this class. Consistency checking of the different + * fields is done in {@link FindFileStructureAction.Request}. + */ +public class FileStructureOverrides { + + public static final FileStructureOverrides EMPTY_OVERRIDES = new Builder().build(); + + private final String charset; + private final FileStructure.Format format; + private final List columnNames; + private final Boolean hasHeaderRow; + private final Character delimiter; + private final Character quote; + private final Boolean shouldTrimFields; + private final String grokPattern; + private final String timestampFormat; + private final String timestampField; + + public FileStructureOverrides(FindFileStructureAction.Request request) { + + this(request.getCharset(), request.getFormat(), request.getColumnNames(), request.getHasHeaderRow(), request.getDelimiter(), + request.getQuote(), request.getShouldTrimFields(), request.getGrokPattern(), request.getTimestampFormat(), + request.getTimestampField()); + } + + private FileStructureOverrides(String charset, FileStructure.Format format, List columnNames, Boolean hasHeaderRow, + Character delimiter, Character quote, Boolean shouldTrimFields, String grokPattern, + String timestampFormat, String timestampField) { + this.charset = charset; + this.format = format; + this.columnNames = (columnNames == null) ? null : Collections.unmodifiableList(new ArrayList<>(columnNames)); + this.hasHeaderRow = hasHeaderRow; + this.delimiter = delimiter; + this.quote = quote; + this.shouldTrimFields = shouldTrimFields; + this.grokPattern = grokPattern; + this.timestampFormat = timestampFormat; + this.timestampField = timestampField; + } + + public static Builder builder() { + return new Builder(); + } + + public String getCharset() { + return charset; + } + + public FileStructure.Format getFormat() { + return format; + } + + public List getColumnNames() { + return columnNames; + } + + public Boolean getHasHeaderRow() { + return hasHeaderRow; + } + + public Character getDelimiter() { + return delimiter; + } + + public Character getQuote() { + return quote; + } + + public Boolean getShouldTrimFields() { + return shouldTrimFields; + } + + public String getGrokPattern() { + return grokPattern; + } + + public String getTimestampFormat() { + return timestampFormat; + } + + public String getTimestampField() { + return timestampField; + } + + @Override + public int hashCode() { + + return Objects.hash(charset, format, columnNames, hasHeaderRow, delimiter, quote, shouldTrimFields, grokPattern, timestampFormat, + timestampField); + } + + @Override + public boolean equals(Object other) { + + if (this == other) { + return true; + } + + if (other == null || getClass() != other.getClass()) { + return false; + } + + FileStructureOverrides that = (FileStructureOverrides) other; + return Objects.equals(this.charset, that.charset) && + Objects.equals(this.format, that.format) && + Objects.equals(this.columnNames, that.columnNames) && + Objects.equals(this.hasHeaderRow, that.hasHeaderRow) && + Objects.equals(this.delimiter, that.delimiter) && + Objects.equals(this.quote, that.quote) && + Objects.equals(this.shouldTrimFields, that.shouldTrimFields) && + Objects.equals(this.grokPattern, that.grokPattern) && + Objects.equals(this.timestampFormat, that.timestampFormat) && + Objects.equals(this.timestampField, that.timestampField); + } + + public static class Builder { + + private String charset; + private FileStructure.Format format; + private List columnNames; + private Boolean hasHeaderRow; + private Character delimiter; + private Character quote; + private Boolean shouldTrimFields; + private String grokPattern; + private String timestampFormat; + private String timestampField; + + public Builder setCharset(String charset) { + this.charset = charset; + return this; + } + + public Builder setFormat(FileStructure.Format format) { + this.format = format; + return this; + } + + public Builder setColumnNames(List columnNames) { + this.columnNames = columnNames; + return this; + } + + public Builder setHasHeaderRow(Boolean hasHeaderRow) { + this.hasHeaderRow = hasHeaderRow; + return this; + } + + public Builder setDelimiter(Character delimiter) { + this.delimiter = delimiter; + return this; + } + + public Builder setQuote(Character quote) { + this.quote = quote; + return this; + } + + public Builder setShouldTrimFields(Boolean shouldTrimFields) { + this.shouldTrimFields = shouldTrimFields; + return this; + } + + public Builder setGrokPattern(String grokPattern) { + this.grokPattern = grokPattern; + return this; + } + + public Builder setTimestampFormat(String timestampFormat) { + this.timestampFormat = timestampFormat; + return this; + } + + public Builder setTimestampField(String timestampField) { + this.timestampField = timestampField; + return this; + } + + public FileStructureOverrides build() { + + return new FileStructureOverrides(charset, format, columnNames, hasHeaderRow, delimiter, quote, shouldTrimFields, grokPattern, + timestampFormat, timestampField); + } + } +} diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureUtils.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureUtils.java index 0341e03a20b..66ecee5b311 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureUtils.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureUtils.java @@ -51,29 +51,41 @@ public final class FileStructureUtils { * may be non-empty when the method is called, and this method may * append to it. * @param sampleRecords List of records derived from the provided sample. + * @param overrides Aspects of the file structure that are known in advance. These take precedence over + * values determined by structure analysis. An exception will be thrown if the file structure + * is incompatible with an overridden value. * @return A tuple of (field name, timestamp format) if one can be found, or null if * there is no consistent timestamp. */ - static Tuple guessTimestampField(List explanation, List> sampleRecords) { + static Tuple guessTimestampField(List explanation, List> sampleRecords, + FileStructureOverrides overrides) { if (sampleRecords.isEmpty()) { return null; } // Accept the first match from the first sample that is compatible with all the other samples - for (Tuple candidate : findCandidates(explanation, sampleRecords)) { + for (Tuple candidate : findCandidates(explanation, sampleRecords, overrides)) { boolean allGood = true; for (Map sampleRecord : sampleRecords.subList(1, sampleRecords.size())) { Object fieldValue = sampleRecord.get(candidate.v1()); if (fieldValue == null) { + if (overrides.getTimestampField() != null) { + throw new IllegalArgumentException("Specified timestamp field [" + overrides.getTimestampField() + + "] is not present in record [" + sampleRecord + "]"); + } explanation.add("First sample match [" + candidate.v1() + "] ruled out because record [" + sampleRecord + "] doesn't have field"); allGood = false; break; } - TimestampMatch match = TimestampFormatFinder.findFirstFullMatch(fieldValue.toString()); + TimestampMatch match = TimestampFormatFinder.findFirstFullMatch(fieldValue.toString(), overrides.getTimestampFormat()); if (match == null || match.candidateIndex != candidate.v2().candidateIndex) { + if (overrides.getTimestampFormat() != null) { + throw new IllegalArgumentException("Specified timestamp format [" + overrides.getTimestampFormat() + + "] does not match for record [" + sampleRecord + "]"); + } explanation.add("First sample match [" + candidate.v1() + "] ruled out because record [" + sampleRecord + "] matches differently: [" + match + "]"); allGood = false; @@ -82,7 +94,8 @@ public final class FileStructureUtils { } if (allGood) { - explanation.add("Guessing timestamp field is [" + candidate.v1() + "] with format [" + candidate.v2() + "]"); + explanation.add(((overrides.getTimestampField() == null) ? "Guessing timestamp" : "Timestamp") + + " field is [" + candidate.v1() + "] with format [" + candidate.v2() + "]"); return candidate; } } @@ -90,23 +103,41 @@ public final class FileStructureUtils { return null; } - private static List> findCandidates(List explanation, List> sampleRecords) { + private static List> findCandidates(List explanation, List> sampleRecords, + FileStructureOverrides overrides) { + + assert sampleRecords.isEmpty() == false; + Map firstRecord = sampleRecords.get(0); + + String onlyConsiderField = overrides.getTimestampField(); + if (onlyConsiderField != null && firstRecord.get(onlyConsiderField) == null) { + throw new IllegalArgumentException("Specified timestamp field [" + overrides.getTimestampField() + + "] is not present in record [" + firstRecord + "]"); + } List> candidates = new ArrayList<>(); - // Get candidate timestamps from the first sample record - for (Map.Entry entry : sampleRecords.get(0).entrySet()) { - Object value = entry.getValue(); - if (value != null) { - TimestampMatch match = TimestampFormatFinder.findFirstFullMatch(value.toString()); - if (match != null) { - Tuple candidate = new Tuple<>(entry.getKey(), match); - candidates.add(candidate); - explanation.add("First sample timestamp match [" + candidate + "]"); + // Get candidate timestamps from the possible field(s) of the first sample record + for (Map.Entry field : firstRecord.entrySet()) { + String fieldName = field.getKey(); + if (onlyConsiderField == null || onlyConsiderField.equals(fieldName)) { + Object value = field.getValue(); + if (value != null) { + TimestampMatch match = TimestampFormatFinder.findFirstFullMatch(value.toString(), overrides.getTimestampFormat()); + if (match != null) { + Tuple candidate = new Tuple<>(fieldName, match); + candidates.add(candidate); + explanation.add("First sample timestamp match [" + candidate + "]"); + } } } } + if (candidates.isEmpty() && overrides.getTimestampFormat() != null) { + throw new IllegalArgumentException("Specified timestamp format [" + overrides.getTimestampFormat() + + "] does not match for record [" + firstRecord + "]"); + } + return candidates; } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/GrokPatternCreator.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/GrokPatternCreator.java index 292d0b8e8b3..54be5079c9d 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/GrokPatternCreator.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/GrokPatternCreator.java @@ -48,21 +48,21 @@ public final class GrokPatternCreator { * Grok patterns that are designed to match the whole message, not just a part of it. */ private static final List FULL_MATCH_GROK_PATTERNS = Arrays.asList( - new FullMatchGrokPatternCandidate("BACULA_LOGLINE", "bts"), - new FullMatchGrokPatternCandidate("CATALINALOG", "timestamp"), - new FullMatchGrokPatternCandidate("COMBINEDAPACHELOG", "timestamp"), - new FullMatchGrokPatternCandidate("COMMONAPACHELOG", "timestamp"), - new FullMatchGrokPatternCandidate("ELB_ACCESS_LOG", "timestamp"), - new FullMatchGrokPatternCandidate("HAPROXYHTTP", "syslog_timestamp"), - new FullMatchGrokPatternCandidate("HAPROXYTCP", "syslog_timestamp"), - new FullMatchGrokPatternCandidate("HTTPD20_ERRORLOG", "timestamp"), - new FullMatchGrokPatternCandidate("HTTPD24_ERRORLOG", "timestamp"), - new FullMatchGrokPatternCandidate("NAGIOSLOGLINE", "nagios_epoch"), - new FullMatchGrokPatternCandidate("NETSCREENSESSIONLOG", "date"), - new FullMatchGrokPatternCandidate("RAILS3", "timestamp"), - new FullMatchGrokPatternCandidate("RUBY_LOGGER", "timestamp"), - new FullMatchGrokPatternCandidate("SHOREWALL", "timestamp"), - new FullMatchGrokPatternCandidate("TOMCATLOG", "timestamp") + FullMatchGrokPatternCandidate.fromGrokPatternName("BACULA_LOGLINE", "bts"), + FullMatchGrokPatternCandidate.fromGrokPatternName("CATALINALOG", "timestamp"), + FullMatchGrokPatternCandidate.fromGrokPatternName("COMBINEDAPACHELOG", "timestamp"), + FullMatchGrokPatternCandidate.fromGrokPatternName("COMMONAPACHELOG", "timestamp"), + FullMatchGrokPatternCandidate.fromGrokPatternName("ELB_ACCESS_LOG", "timestamp"), + FullMatchGrokPatternCandidate.fromGrokPatternName("HAPROXYHTTP", "syslog_timestamp"), + FullMatchGrokPatternCandidate.fromGrokPatternName("HAPROXYTCP", "syslog_timestamp"), + FullMatchGrokPatternCandidate.fromGrokPatternName("HTTPD20_ERRORLOG", "timestamp"), + FullMatchGrokPatternCandidate.fromGrokPatternName("HTTPD24_ERRORLOG", "timestamp"), + FullMatchGrokPatternCandidate.fromGrokPatternName("NAGIOSLOGLINE", "nagios_epoch"), + FullMatchGrokPatternCandidate.fromGrokPatternName("NETSCREENSESSIONLOG", "date"), + FullMatchGrokPatternCandidate.fromGrokPatternName("RAILS3", "timestamp"), + FullMatchGrokPatternCandidate.fromGrokPatternName("RUBY_LOGGER", "timestamp"), + FullMatchGrokPatternCandidate.fromGrokPatternName("SHOREWALL", "timestamp"), + FullMatchGrokPatternCandidate.fromGrokPatternName("TOMCATLOG", "timestamp") ); /** @@ -87,7 +87,7 @@ public final class GrokPatternCreator { // Can't use \b as the breaks, because slashes are not "word" characters new ValueOnlyGrokPatternCandidate("PATH", "keyword", "path", "(?null. + * @param timestampField If not null then the chosen Grok pattern must use this timestamp field. * @return A tuple of (time field name, Grok string), or null if no suitable Grok pattern was found. */ - public Tuple findFullLineGrokPattern() { + public Tuple findFullLineGrokPattern(String timestampField) { for (FullMatchGrokPatternCandidate candidate : FULL_MATCH_GROK_PATTERNS) { - if (candidate.matchesAll(sampleMessages)) { - return candidate.processMatch(explanation, sampleMessages, mappings, fieldStats); + if (timestampField == null || timestampField.equals(candidate.getTimeField())) { + if (candidate.matchesAll(sampleMessages)) { + return candidate.processMatch(explanation, sampleMessages, mappings, fieldStats); + } } } return null; } + /** + * This method processes a user-supplied Grok pattern that will match all of the sample messages in their entirety. + * It will also update mappings and field stats if they are non-null. + * @param grokPattern The user supplied Grok pattern. + * @param timestampField The name of the timestamp field within the Grok pattern. + * @throws IllegalArgumentException If the supplied Grok pattern does not match the sample messages. + */ + public void validateFullLineGrokPattern(String grokPattern, String timestampField) { + + FullMatchGrokPatternCandidate candidate = FullMatchGrokPatternCandidate.fromGrokPattern(grokPattern, timestampField); + if (candidate.matchesAll(sampleMessages)) { + candidate.processMatch(explanation, sampleMessages, mappings, fieldStats); + } else { + throw new IllegalArgumentException("Supplied Grok pattern [" + grokPattern + "] does not match sample messages"); + } + } + /** * Build a Grok pattern that will match all of the sample messages in their entirety. * @param seedPatternName A pattern that has already been determined to match some portion of every sample message. @@ -564,14 +584,26 @@ public final class GrokPatternCreator { */ static class FullMatchGrokPatternCandidate { - private final String grokString; + private final String grokPattern; private final String timeField; private final Grok grok; - FullMatchGrokPatternCandidate(String grokPatternName, String timeField) { - grokString = "%{" + grokPatternName + "}"; + static FullMatchGrokPatternCandidate fromGrokPatternName(String grokPatternName, String timeField) { + return new FullMatchGrokPatternCandidate("%{" + grokPatternName + "}", timeField); + } + + static FullMatchGrokPatternCandidate fromGrokPattern(String grokPattern, String timeField) { + return new FullMatchGrokPatternCandidate(grokPattern, timeField); + } + + private FullMatchGrokPatternCandidate(String grokPattern, String timeField) { + this.grokPattern = grokPattern; this.timeField = timeField; - grok = new Grok(Grok.getBuiltinPatterns(), grokString); + grok = new Grok(Grok.getBuiltinPatterns(), grokPattern); + } + + public String getTimeField() { + return timeField; } public boolean matchesAll(Collection sampleMessages) { @@ -585,7 +617,7 @@ public final class GrokPatternCreator { public Tuple processMatch(List explanation, Collection sampleMessages, Map mappings, Map fieldStats) { - explanation.add("A full message Grok pattern [" + grokString.substring(2, grokString.length() - 1) + "] looks appropriate"); + explanation.add("A full message Grok pattern [" + grokPattern.substring(2, grokPattern.length() - 1) + "] looks appropriate"); if (mappings != null || fieldStats != null) { Map> valuesPerField = new HashMap<>(); @@ -594,41 +626,39 @@ public final class GrokPatternCreator { Map captures = grok.captures(sampleMessage); // If the pattern doesn't match then captures will be null if (captures == null) { - throw new IllegalStateException("[" + grokString + "] does not match snippet [" + sampleMessage + "]"); + throw new IllegalStateException("[" + grokPattern + "] does not match snippet [" + sampleMessage + "]"); } for (Map.Entry capture : captures.entrySet()) { String fieldName = capture.getKey(); String fieldValue = capture.getValue().toString(); - - // Exclude the time field because that will be dropped and replaced with @timestamp - if (fieldName.equals(timeField) == false) { - valuesPerField.compute(fieldName, (k, v) -> { - if (v == null) { - return new ArrayList<>(Collections.singletonList(fieldValue)); - } else { - v.add(fieldValue); - return v; - } - }); - } + valuesPerField.compute(fieldName, (k, v) -> { + if (v == null) { + return new ArrayList<>(Collections.singletonList(fieldValue)); + } else { + v.add(fieldValue); + return v; + } + }); } } for (Map.Entry> valuesForField : valuesPerField.entrySet()) { String fieldName = valuesForField.getKey(); if (mappings != null) { - mappings.put(fieldName, - FileStructureUtils.guessScalarMapping(explanation, fieldName, valuesForField.getValue())); + // Exclude the time field because that will be dropped and replaced with @timestamp + if (fieldName.equals(timeField) == false) { + mappings.put(fieldName, + FileStructureUtils.guessScalarMapping(explanation, fieldName, valuesForField.getValue())); + } } if (fieldStats != null) { - fieldStats.put(fieldName, - FileStructureUtils.calculateFieldStats(valuesForField.getValue())); + fieldStats.put(fieldName, FileStructureUtils.calculateFieldStats(valuesForField.getValue())); } } } - return new Tuple<>(timeField, grokString); + return new Tuple<>(timeField, grokPattern); } } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/JsonFileStructureFinder.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/JsonFileStructureFinder.java index a488549bc52..b20658f872b 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/JsonFileStructureFinder.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/JsonFileStructureFinder.java @@ -33,7 +33,8 @@ public class JsonFileStructureFinder implements FileStructureFinder { private final FileStructure structure; static JsonFileStructureFinder makeJsonFileStructureFinder(List explanation, String sample, String charsetName, - Boolean hasByteOrderMarker) throws IOException { + Boolean hasByteOrderMarker, FileStructureOverrides overrides) + throws IOException { List> sampleRecords = new ArrayList<>(); @@ -51,7 +52,7 @@ public class JsonFileStructureFinder implements FileStructureFinder { .setNumLinesAnalyzed(sampleMessages.size()) .setNumMessagesAnalyzed(sampleRecords.size()); - Tuple timeField = FileStructureUtils.guessTimestampField(explanation, sampleRecords); + Tuple timeField = FileStructureUtils.guessTimestampField(explanation, sampleRecords, overrides); if (timeField != null) { structureBuilder.setTimestampField(timeField.v1()) .setTimestampFormats(timeField.v2().dateFormats) @@ -62,7 +63,10 @@ public class JsonFileStructureFinder implements FileStructureFinder { FileStructureUtils.guessMappingsAndCalculateFieldStats(explanation, sampleRecords); SortedMap mappings = mappingsAndFieldStats.v1(); - mappings.put(FileStructureUtils.DEFAULT_TIMESTAMP_FIELD, Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "date")); + if (timeField != null) { + mappings.put(FileStructureUtils.DEFAULT_TIMESTAMP_FIELD, + Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "date")); + } if (mappingsAndFieldStats.v2() != null) { structureBuilder.setFieldStats(mappingsAndFieldStats.v2()); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/JsonFileStructureFinderFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/JsonFileStructureFinderFactory.java index 02be3c1cf19..cfeaa222679 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/JsonFileStructureFinderFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/JsonFileStructureFinderFactory.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.ml.filestructurefinder; import org.elasticsearch.common.xcontent.DeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.xpack.core.ml.filestructurefinder.FileStructure; import java.io.IOException; import java.io.StringReader; @@ -18,6 +19,11 @@ import static org.elasticsearch.common.xcontent.json.JsonXContent.jsonXContent; public class JsonFileStructureFinderFactory implements FileStructureFinderFactory { + @Override + public boolean canFindFormat(FileStructure.Format format) { + return format == null || format == FileStructure.Format.JSON; + } + /** * This format matches if the sample consists of one or more JSON documents. * If there is more than one, they must be newline-delimited. The @@ -61,9 +67,9 @@ public class JsonFileStructureFinderFactory implements FileStructureFinderFactor } @Override - public FileStructureFinder createFromSample(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker) - throws IOException { - return JsonFileStructureFinder.makeJsonFileStructureFinder(explanation, sample, charsetName, hasByteOrderMarker); + public FileStructureFinder createFromSample(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker, + FileStructureOverrides overrides) throws IOException { + return JsonFileStructureFinder.makeJsonFileStructureFinder(explanation, sample, charsetName, hasByteOrderMarker, overrides); } private static class ContextPrintingStringReader extends StringReader { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/TextLogFileStructureFinder.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/TextLogFileStructureFinder.java index 95e0a5dc69d..e6e445a3ff6 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/TextLogFileStructureFinder.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/TextLogFileStructureFinder.java @@ -28,17 +28,19 @@ public class TextLogFileStructureFinder implements FileStructureFinder { private final FileStructure structure; static TextLogFileStructureFinder makeTextLogFileStructureFinder(List explanation, String sample, String charsetName, - Boolean hasByteOrderMarker) { + Boolean hasByteOrderMarker, FileStructureOverrides overrides) { String[] sampleLines = sample.split("\n"); - Tuple> bestTimestamp = mostLikelyTimestamp(sampleLines); + Tuple> bestTimestamp = mostLikelyTimestamp(sampleLines, overrides); if (bestTimestamp == null) { // Is it appropriate to treat a file that is neither structured nor has // a regular pattern of timestamps as a log file? Probably not... - throw new IllegalArgumentException("Could not find a timestamp in the sample provided"); + throw new IllegalArgumentException("Could not find " + + ((overrides.getTimestampFormat() == null) ? "a timestamp" : "the specified timestamp format") + " in the sample provided"); } - explanation.add("Most likely timestamp format is [" + bestTimestamp.v1() + "]"); + explanation.add(((overrides.getTimestampFormat() == null) ? "Most likely timestamp" : "Timestamp") + " format is [" + + bestTimestamp.v1() + "]"); List sampleMessages = new ArrayList<>(); StringBuilder preamble = new StringBuilder(); @@ -86,17 +88,26 @@ public class TextLogFileStructureFinder implements FileStructureFinder { SortedMap fieldStats = new TreeMap<>(); - // We can't parse directly into @timestamp using Grok, so parse to some other time field, which the date filter will then remove - String interimTimestampField; - String grokPattern; GrokPatternCreator grokPatternCreator = new GrokPatternCreator(explanation, sampleMessages, mappings, fieldStats); - Tuple timestampFieldAndFullMatchGrokPattern = grokPatternCreator.findFullLineGrokPattern(); - if (timestampFieldAndFullMatchGrokPattern != null) { - interimTimestampField = timestampFieldAndFullMatchGrokPattern.v1(); - grokPattern = timestampFieldAndFullMatchGrokPattern.v2(); + // We can't parse directly into @timestamp using Grok, so parse to some other time field, which the date filter will then remove + String interimTimestampField = overrides.getTimestampField(); + String grokPattern = overrides.getGrokPattern(); + if (grokPattern != null) { + if (interimTimestampField == null) { + interimTimestampField = "timestamp"; + } + grokPatternCreator.validateFullLineGrokPattern(grokPattern, interimTimestampField); } else { - interimTimestampField = "timestamp"; - grokPattern = grokPatternCreator.createGrokPatternFromExamples(bestTimestamp.v1().grokPatternName, interimTimestampField); + Tuple timestampFieldAndFullMatchGrokPattern = grokPatternCreator.findFullLineGrokPattern(interimTimestampField); + if (timestampFieldAndFullMatchGrokPattern != null) { + interimTimestampField = timestampFieldAndFullMatchGrokPattern.v1(); + grokPattern = timestampFieldAndFullMatchGrokPattern.v2(); + } else { + if (interimTimestampField == null) { + interimTimestampField = "timestamp"; + } + grokPattern = grokPatternCreator.createGrokPatternFromExamples(bestTimestamp.v1().grokPatternName, interimTimestampField); + } } FileStructure structure = structureBuilder @@ -127,14 +138,14 @@ public class TextLogFileStructureFinder implements FileStructureFinder { return structure; } - static Tuple> mostLikelyTimestamp(String[] sampleLines) { + static Tuple> mostLikelyTimestamp(String[] sampleLines, FileStructureOverrides overrides) { Map>> timestampMatches = new LinkedHashMap<>(); int remainingLines = sampleLines.length; double differenceBetweenTwoHighestWeights = 0.0; for (String sampleLine : sampleLines) { - TimestampMatch match = TimestampFormatFinder.findFirstMatch(sampleLine); + TimestampMatch match = TimestampFormatFinder.findFirstMatch(sampleLine, overrides.getTimestampFormat()); if (match != null) { TimestampMatch pureMatch = new TimestampMatch(match.candidateIndex, "", match.dateFormats, match.simplePattern, match.grokPatternName, ""); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/TextLogFileStructureFinderFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/TextLogFileStructureFinderFactory.java index 5f737eeb9b8..b92b705aaff 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/TextLogFileStructureFinderFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/TextLogFileStructureFinderFactory.java @@ -5,6 +5,8 @@ */ package org.elasticsearch.xpack.ml.filestructurefinder; +import org.elasticsearch.xpack.core.ml.filestructurefinder.FileStructure; + import java.util.List; import java.util.regex.Pattern; @@ -13,6 +15,11 @@ public class TextLogFileStructureFinderFactory implements FileStructureFinderFac // This works because, by default, dot doesn't match newlines private static final Pattern TWO_NON_BLANK_LINES_PATTERN = Pattern.compile(".\n+."); + @Override + public boolean canFindFormat(FileStructure.Format format) { + return format == null || format == FileStructure.Format.SEMI_STRUCTURED_TEXT; + } + /** * This format matches if the sample contains at least one newline and at least two * non-blank lines. @@ -33,7 +40,9 @@ public class TextLogFileStructureFinderFactory implements FileStructureFinderFac } @Override - public FileStructureFinder createFromSample(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker) { - return TextLogFileStructureFinder.makeTextLogFileStructureFinder(explanation, sample, charsetName, hasByteOrderMarker); + public FileStructureFinder createFromSample(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker, + FileStructureOverrides overrides) { + return TextLogFileStructureFinder.makeTextLogFileStructureFinder(explanation, sample, charsetName, hasByteOrderMarker, + overrides); } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/TimestampFormatFinder.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/TimestampFormatFinder.java index 81e490878a0..363b1352a54 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/TimestampFormatFinder.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/TimestampFormatFinder.java @@ -148,6 +148,16 @@ public final class TimestampFormatFinder { return findFirstMatch(text, 0); } + /** + * Find the first timestamp format that matches part of the supplied value. + * @param text The value that the returned timestamp format must exist within. + * @param requiredFormat A date format that any returned match must support. + * @return The timestamp format, or null if none matches. + */ + public static TimestampMatch findFirstMatch(String text, String requiredFormat) { + return findFirstMatch(text, 0, requiredFormat); + } + /** * Find the first timestamp format that matches part of the supplied value, * excluding a specified number of candidate formats. @@ -156,26 +166,40 @@ public final class TimestampFormatFinder { * @return The timestamp format, or null if none matches. */ public static TimestampMatch findFirstMatch(String text, int ignoreCandidates) { + return findFirstMatch(text, ignoreCandidates, null); + } + + /** + * Find the first timestamp format that matches part of the supplied value, + * excluding a specified number of candidate formats. + * @param text The value that the returned timestamp format must exist within. + * @param ignoreCandidates The number of candidate formats to exclude from the search. + * @param requiredFormat A date format that any returned match must support. + * @return The timestamp format, or null if none matches. + */ + public static TimestampMatch findFirstMatch(String text, int ignoreCandidates, String requiredFormat) { Boolean[] quickRuleoutMatches = new Boolean[QUICK_RULE_OUT_PATTERNS.size()]; int index = ignoreCandidates; for (CandidateTimestampFormat candidate : ORDERED_CANDIDATE_FORMATS.subList(ignoreCandidates, ORDERED_CANDIDATE_FORMATS.size())) { - boolean quicklyRuledOut = false; - for (Integer quickRuleOutIndex : candidate.quickRuleOutIndices) { - if (quickRuleoutMatches[quickRuleOutIndex] == null) { - quickRuleoutMatches[quickRuleOutIndex] = QUICK_RULE_OUT_PATTERNS.get(quickRuleOutIndex).matcher(text).find(); + if (requiredFormat == null || candidate.dateFormats.contains(requiredFormat)) { + boolean quicklyRuledOut = false; + for (Integer quickRuleOutIndex : candidate.quickRuleOutIndices) { + if (quickRuleoutMatches[quickRuleOutIndex] == null) { + quickRuleoutMatches[quickRuleOutIndex] = QUICK_RULE_OUT_PATTERNS.get(quickRuleOutIndex).matcher(text).find(); + } + if (quickRuleoutMatches[quickRuleOutIndex] == false) { + quicklyRuledOut = true; + break; + } } - if (quickRuleoutMatches[quickRuleOutIndex] == false) { - quicklyRuledOut = true; - break; - } - } - if (quicklyRuledOut == false) { - Map captures = candidate.strictSearchGrok.captures(text); - if (captures != null) { - String preface = captures.getOrDefault(PREFACE, "").toString(); - String epilogue = captures.getOrDefault(EPILOGUE, "").toString(); - return makeTimestampMatch(candidate, index, preface, text.substring(preface.length(), - text.length() - epilogue.length()), epilogue); + if (quicklyRuledOut == false) { + Map captures = candidate.strictSearchGrok.captures(text); + if (captures != null) { + String preface = captures.getOrDefault(PREFACE, "").toString(); + String epilogue = captures.getOrDefault(EPILOGUE, "").toString(); + return makeTimestampMatch(candidate, index, preface, text.substring(preface.length(), + text.length() - epilogue.length()), epilogue); + } } } ++index; @@ -192,6 +216,16 @@ public final class TimestampFormatFinder { return findFirstFullMatch(text, 0); } + /** + * Find the best timestamp format for matching an entire field value. + * @param text The value that the returned timestamp format must match in its entirety. + * @param requiredFormat A date format that any returned match must support. + * @return The timestamp format, or null if none matches. + */ + public static TimestampMatch findFirstFullMatch(String text, String requiredFormat) { + return findFirstFullMatch(text, 0, requiredFormat); + } + /** * Find the best timestamp format for matching an entire field value, * excluding a specified number of candidate formats. @@ -200,11 +234,25 @@ public final class TimestampFormatFinder { * @return The timestamp format, or null if none matches. */ public static TimestampMatch findFirstFullMatch(String text, int ignoreCandidates) { + return findFirstFullMatch(text, ignoreCandidates, null); + } + + /** + * Find the best timestamp format for matching an entire field value, + * excluding a specified number of candidate formats. + * @param text The value that the returned timestamp format must match in its entirety. + * @param ignoreCandidates The number of candidate formats to exclude from the search. + * @param requiredFormat A date format that any returned match must support. + * @return The timestamp format, or null if none matches. + */ + public static TimestampMatch findFirstFullMatch(String text, int ignoreCandidates, String requiredFormat) { int index = ignoreCandidates; for (CandidateTimestampFormat candidate : ORDERED_CANDIDATE_FORMATS.subList(ignoreCandidates, ORDERED_CANDIDATE_FORMATS.size())) { - Map captures = candidate.strictFullMatchGrok.captures(text); - if (captures != null) { - return makeTimestampMatch(candidate, index, "", text, ""); + if (requiredFormat == null || candidate.dateFormats.contains(requiredFormat)) { + Map captures = candidate.strictFullMatchGrok.captures(text); + if (captures != null) { + return makeTimestampMatch(candidate, index, "", text, ""); + } } ++index; } @@ -417,7 +465,7 @@ public final class TimestampFormatFinder { // The (?m) here has the Ruby meaning, which is equivalent to (?s) in Java this.strictSearchGrok = new Grok(Grok.getBuiltinPatterns(), "(?m)%{DATA:" + PREFACE + "}" + strictGrokPattern + "%{GREEDYDATA:" + EPILOGUE + "}"); - this.strictFullMatchGrok = new Grok(Grok.getBuiltinPatterns(), strictGrokPattern); + this.strictFullMatchGrok = new Grok(Grok.getBuiltinPatterns(), "^" + strictGrokPattern + "$"); this.standardGrokPatternName = standardGrokPatternName; assert quickRuleOutIndices.stream() .noneMatch(quickRuleOutIndex -> quickRuleOutIndex < 0 || quickRuleOutIndex >= QUICK_RULE_OUT_PATTERNS.size()); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/XmlFileStructureFinder.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/XmlFileStructureFinder.java index 570f36f59c0..d5e3fba34c9 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/XmlFileStructureFinder.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/XmlFileStructureFinder.java @@ -38,7 +38,7 @@ public class XmlFileStructureFinder implements FileStructureFinder { private final FileStructure structure; static XmlFileStructureFinder makeXmlFileStructureFinder(List explanation, String sample, String charsetName, - Boolean hasByteOrderMarker) + Boolean hasByteOrderMarker, FileStructureOverrides overrides) throws IOException, ParserConfigurationException, SAXException { String messagePrefix; @@ -90,7 +90,7 @@ public class XmlFileStructureFinder implements FileStructureFinder { .setNumMessagesAnalyzed(sampleRecords.size()) .setMultilineStartPattern("^\\s*<" + topLevelTag); - Tuple timeField = FileStructureUtils.guessTimestampField(explanation, sampleRecords); + Tuple timeField = FileStructureUtils.guessTimestampField(explanation, sampleRecords, overrides); if (timeField != null) { structureBuilder.setTimestampField(timeField.v1()) .setTimestampFormats(timeField.v2().dateFormats) @@ -110,8 +110,10 @@ public class XmlFileStructureFinder implements FileStructureFinder { secondLevelProperties.put(FileStructureUtils.MAPPING_PROPERTIES_SETTING, innerMappings); SortedMap outerMappings = new TreeMap<>(); outerMappings.put(topLevelTag, secondLevelProperties); - outerMappings.put(FileStructureUtils.DEFAULT_TIMESTAMP_FIELD, - Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "date")); + if (timeField != null) { + outerMappings.put(FileStructureUtils.DEFAULT_TIMESTAMP_FIELD, + Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "date")); + } FileStructure structure = structureBuilder .setMappings(outerMappings) diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/XmlFileStructureFinderFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/XmlFileStructureFinderFactory.java index f8536d14375..3079f53931d 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/XmlFileStructureFinderFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/XmlFileStructureFinderFactory.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.ml.filestructurefinder; +import org.elasticsearch.xpack.core.ml.filestructurefinder.FileStructure; import org.xml.sax.SAXException; import javax.xml.parsers.ParserConfigurationException; @@ -27,6 +28,11 @@ public class XmlFileStructureFinderFactory implements FileStructureFinderFactory xmlFactory.setProperty(XMLInputFactory.IS_VALIDATING, Boolean.FALSE); } + @Override + public boolean canFindFormat(FileStructure.Format format) { + return format == null || format == FileStructure.Format.XML; + } + /** * This format matches if the sample consists of one or more XML documents, * all with the same root element name. If there is more than one document, @@ -115,8 +121,9 @@ public class XmlFileStructureFinderFactory implements FileStructureFinderFactory } @Override - public FileStructureFinder createFromSample(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker) + public FileStructureFinder createFromSample(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker, + FileStructureOverrides overrides) throws IOException, ParserConfigurationException, SAXException { - return XmlFileStructureFinder.makeXmlFileStructureFinder(explanation, sample, charsetName, hasByteOrderMarker); + return XmlFileStructureFinder.makeXmlFileStructureFinder(explanation, sample, charsetName, hasByteOrderMarker, overrides); } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/RestFindFileStructureAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/RestFindFileStructureAction.java index 83293c7d60e..316a4b56e4a 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/RestFindFileStructureAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/RestFindFileStructureAction.java @@ -39,6 +39,17 @@ public class RestFindFileStructureAction extends BaseRestHandler { FindFileStructureAction.Request request = new FindFileStructureAction.Request(); request.setLinesToSample(restRequest.paramAsInt(FindFileStructureAction.Request.LINES_TO_SAMPLE.getPreferredName(), FileStructureFinderManager.DEFAULT_IDEAL_SAMPLE_LINE_COUNT)); + request.setCharset(restRequest.param(FindFileStructureAction.Request.CHARSET.getPreferredName())); + request.setFormat(restRequest.param(FindFileStructureAction.Request.FORMAT.getPreferredName())); + request.setColumnNames(restRequest.paramAsStringArray(FindFileStructureAction.Request.COLUMN_NAMES.getPreferredName(), null)); + request.setHasHeaderRow(restRequest.paramAsBoolean(FindFileStructureAction.Request.HAS_HEADER_ROW.getPreferredName(), null)); + request.setDelimiter(restRequest.param(FindFileStructureAction.Request.DELIMITER.getPreferredName())); + request.setQuote(restRequest.param(FindFileStructureAction.Request.QUOTE.getPreferredName())); + request.setShouldTrimFields(restRequest.paramAsBoolean(FindFileStructureAction.Request.SHOULD_TRIM_FIELDS.getPreferredName(), + null)); + request.setGrokPattern(restRequest.param(FindFileStructureAction.Request.GROK_PATTERN.getPreferredName())); + request.setTimestampFormat(restRequest.param(FindFileStructureAction.Request.TIMESTAMP_FORMAT.getPreferredName())); + request.setTimestampField(restRequest.param(FindFileStructureAction.Request.TIMESTAMP_FIELD.getPreferredName())); if (restRequest.hasContent()) { request.setSample(restRequest.content()); } else { diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinderFactoryTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinderFactoryTests.java index 6bcb827be94..53f3a2a4d4c 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinderFactoryTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinderFactoryTests.java @@ -7,10 +7,10 @@ package org.elasticsearch.xpack.ml.filestructurefinder; public class DelimitedFileStructureFinderFactoryTests extends FileStructureTestCase { - private FileStructureFinderFactory csvFactory = new DelimitedFileStructureFinderFactory(',', 2, false); - private FileStructureFinderFactory tsvFactory = new DelimitedFileStructureFinderFactory('\t', 2, false); - private FileStructureFinderFactory semiColonDelimitedfactory = new DelimitedFileStructureFinderFactory(';', 4, false); - private FileStructureFinderFactory pipeDelimitedFactory = new DelimitedFileStructureFinderFactory('|', 5, true); + private FileStructureFinderFactory csvFactory = new DelimitedFileStructureFinderFactory(',', '"', 2, false); + private FileStructureFinderFactory tsvFactory = new DelimitedFileStructureFinderFactory('\t', '"', 2, false); + private FileStructureFinderFactory semiColonDelimitedfactory = new DelimitedFileStructureFinderFactory(';', '"', 4, false); + private FileStructureFinderFactory pipeDelimitedFactory = new DelimitedFileStructureFinderFactory('|', '"', 5, true); // CSV - no need to check JSON or XML because they come earlier in the order we check formats diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinderTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinderTests.java index 4e692d58391..decc61a5397 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinderTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinderTests.java @@ -19,7 +19,7 @@ import static org.hamcrest.Matchers.arrayContaining; public class DelimitedFileStructureFinderTests extends FileStructureTestCase { - private FileStructureFinderFactory csvFactory = new DelimitedFileStructureFinderFactory(',', 2, false); + private FileStructureFinderFactory csvFactory = new DelimitedFileStructureFinderFactory(',', '"', 2, false); public void testCreateConfigsGivenCompleteCsv() throws Exception { String sample = "time,message\n" + @@ -29,7 +29,8 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { String charset = randomFrom(POSSIBLE_CHARSETS); Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); - FileStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker); + FileStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker, + FileStructureOverrides.EMPTY_OVERRIDES); FileStructure structure = structureFinder.getStructure(); @@ -43,6 +44,7 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { assertEquals("^\"?time\"?,\"?message\"?", structure.getExcludeLinesPattern()); assertEquals("^\"?\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}", structure.getMultilineStartPattern()); assertEquals(Character.valueOf(','), structure.getDelimiter()); + assertEquals(Character.valueOf('"'), structure.getQuote()); assertTrue(structure.getHasHeaderRow()); assertNull(structure.getShouldTrimFields()); assertEquals(Arrays.asList("time", "message"), structure.getColumnNames()); @@ -51,6 +53,76 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { assertEquals(Collections.singletonList("ISO8601"), structure.getTimestampFormats()); } + public void testCreateConfigsGivenCompleteCsvAndColumnNamesOverride() throws Exception { + + FileStructureOverrides overrides = FileStructureOverrides.builder().setColumnNames(Arrays.asList("my_time", "my_message")).build(); + + String sample = "time,message\n" + + "2018-05-17T13:41:23,hello\n" + + "2018-05-17T13:41:32,hello again\n"; + assertTrue(csvFactory.canCreateFromSample(explanation, sample)); + + String charset = randomFrom(POSSIBLE_CHARSETS); + Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); + FileStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker, overrides); + + FileStructure structure = structureFinder.getStructure(); + + assertEquals(FileStructure.Format.DELIMITED, structure.getFormat()); + assertEquals(charset, structure.getCharset()); + if (hasByteOrderMarker == null) { + assertNull(structure.getHasByteOrderMarker()); + } else { + assertEquals(hasByteOrderMarker, structure.getHasByteOrderMarker()); + } + assertEquals("^\"?time\"?,\"?message\"?", structure.getExcludeLinesPattern()); + assertEquals("^\"?\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}", structure.getMultilineStartPattern()); + assertEquals(Character.valueOf(','), structure.getDelimiter()); + assertEquals(Character.valueOf('"'), structure.getQuote()); + assertTrue(structure.getHasHeaderRow()); + assertNull(structure.getShouldTrimFields()); + assertEquals(Arrays.asList("my_time", "my_message"), structure.getColumnNames()); + assertNull(structure.getGrokPattern()); + assertEquals("my_time", structure.getTimestampField()); + assertEquals(Collections.singletonList("ISO8601"), structure.getTimestampFormats()); + } + + public void testCreateConfigsGivenCompleteCsvAndHasHeaderRowOverride() throws Exception { + + // It's obvious the first row really should be a header row, so by overriding + // detection with the wrong choice the results will be completely changed + FileStructureOverrides overrides = FileStructureOverrides.builder().setHasHeaderRow(false).build(); + + String sample = "time,message\n" + + "2018-05-17T13:41:23,hello\n" + + "2018-05-17T13:41:32,hello again\n"; + assertTrue(csvFactory.canCreateFromSample(explanation, sample)); + + String charset = randomFrom(POSSIBLE_CHARSETS); + Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); + FileStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker, overrides); + + FileStructure structure = structureFinder.getStructure(); + + assertEquals(FileStructure.Format.DELIMITED, structure.getFormat()); + assertEquals(charset, structure.getCharset()); + if (hasByteOrderMarker == null) { + assertNull(structure.getHasByteOrderMarker()); + } else { + assertEquals(hasByteOrderMarker, structure.getHasByteOrderMarker()); + } + assertNull(structure.getExcludeLinesPattern()); + assertNull(structure.getMultilineStartPattern()); + assertEquals(Character.valueOf(','), structure.getDelimiter()); + assertEquals(Character.valueOf('"'), structure.getQuote()); + assertFalse(structure.getHasHeaderRow()); + assertNull(structure.getShouldTrimFields()); + assertEquals(Arrays.asList("column1", "column2"), structure.getColumnNames()); + assertNull(structure.getGrokPattern()); + assertNull(structure.getTimestampField()); + assertNull(structure.getTimestampFormats()); + } + public void testCreateConfigsGivenCsvWithIncompleteLastRecord() throws Exception { String sample = "message,time,count\n" + "\"hello\n" + @@ -60,7 +132,8 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { String charset = randomFrom(POSSIBLE_CHARSETS); Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); - FileStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker); + FileStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker, + FileStructureOverrides.EMPTY_OVERRIDES); FileStructure structure = structureFinder.getStructure(); @@ -74,6 +147,7 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { assertEquals("^\"?message\"?,\"?time\"?,\"?count\"?", structure.getExcludeLinesPattern()); assertEquals("^.*?,\"?\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}", structure.getMultilineStartPattern()); assertEquals(Character.valueOf(','), structure.getDelimiter()); + assertEquals(Character.valueOf('"'), structure.getQuote()); assertTrue(structure.getHasHeaderRow()); assertNull(structure.getShouldTrimFields()); assertEquals(Arrays.asList("message", "time", "count"), structure.getColumnNames()); @@ -93,7 +167,8 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { String charset = randomFrom(POSSIBLE_CHARSETS); Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); - FileStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker); + FileStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker, + FileStructureOverrides.EMPTY_OVERRIDES); FileStructure structure = structureFinder.getStructure(); @@ -110,6 +185,7 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { structure.getExcludeLinesPattern()); assertEquals("^.*?,\"?\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}", structure.getMultilineStartPattern()); assertEquals(Character.valueOf(','), structure.getDelimiter()); + assertEquals(Character.valueOf('"'), structure.getQuote()); assertTrue(structure.getHasHeaderRow()); assertNull(structure.getShouldTrimFields()); assertEquals(Arrays.asList("VendorID", "tpep_pickup_datetime", "tpep_dropoff_datetime", "passenger_count", "trip_distance", @@ -120,6 +196,50 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { assertEquals(Collections.singletonList("YYYY-MM-dd HH:mm:ss"), structure.getTimestampFormats()); } + public void testCreateConfigsGivenCsvWithTrailingNullsAndOverriddenTimeField() throws Exception { + + // Default timestamp field is the first field from the start of each row that contains a + // consistent timestamp format, so if we want the second we need an override + FileStructureOverrides overrides = FileStructureOverrides.builder().setTimestampField("tpep_dropoff_datetime").build(); + + String sample = "VendorID,tpep_pickup_datetime,tpep_dropoff_datetime,passenger_count,trip_distance,RatecodeID," + + "store_and_fwd_flag,PULocationID,DOLocationID,payment_type,fare_amount,extra,mta_tax,tip_amount,tolls_amount," + + "improvement_surcharge,total_amount,,\n" + + "2,2016-12-31 15:15:01,2016-12-31 15:15:09,1,.00,1,N,264,264,2,1,0,0.5,0,0,0.3,1.8,,\n" + + "1,2016-12-01 00:00:01,2016-12-01 00:10:22,1,1.60,1,N,163,143,2,9,0.5,0.5,0,0,0.3,10.3,,\n" + + "1,2016-12-01 00:00:01,2016-12-01 00:11:01,1,1.40,1,N,164,229,1,9,0.5,0.5,2.05,0,0.3,12.35,,\n"; + assertTrue(csvFactory.canCreateFromSample(explanation, sample)); + + String charset = randomFrom(POSSIBLE_CHARSETS); + Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); + FileStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker, overrides); + + FileStructure structure = structureFinder.getStructure(); + + assertEquals(FileStructure.Format.DELIMITED, structure.getFormat()); + assertEquals(charset, structure.getCharset()); + if (hasByteOrderMarker == null) { + assertNull(structure.getHasByteOrderMarker()); + } else { + assertEquals(hasByteOrderMarker, structure.getHasByteOrderMarker()); + } + assertEquals("^\"?VendorID\"?,\"?tpep_pickup_datetime\"?,\"?tpep_dropoff_datetime\"?,\"?passenger_count\"?,\"?trip_distance\"?," + + "\"?RatecodeID\"?,\"?store_and_fwd_flag\"?,\"?PULocationID\"?,\"?DOLocationID\"?,\"?payment_type\"?,\"?fare_amount\"?," + + "\"?extra\"?,\"?mta_tax\"?,\"?tip_amount\"?,\"?tolls_amount\"?,\"?improvement_surcharge\"?,\"?total_amount\"?,\"?\"?,\"?\"?", + structure.getExcludeLinesPattern()); + assertEquals("^.*?,.*?,\"?\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}", structure.getMultilineStartPattern()); + assertEquals(Character.valueOf(','), structure.getDelimiter()); + assertEquals(Character.valueOf('"'), structure.getQuote()); + assertTrue(structure.getHasHeaderRow()); + assertNull(structure.getShouldTrimFields()); + assertEquals(Arrays.asList("VendorID", "tpep_pickup_datetime", "tpep_dropoff_datetime", "passenger_count", "trip_distance", + "RatecodeID", "store_and_fwd_flag", "PULocationID", "DOLocationID", "payment_type", "fare_amount", "extra", "mta_tax", + "tip_amount", "tolls_amount", "improvement_surcharge", "total_amount", "column18", "column19"), structure.getColumnNames()); + assertNull(structure.getGrokPattern()); + assertEquals("tpep_dropoff_datetime", structure.getTimestampField()); + assertEquals(Collections.singletonList("YYYY-MM-dd HH:mm:ss"), structure.getTimestampFormats()); + } + public void testCreateConfigsGivenCsvWithTrailingNullsExceptHeader() throws Exception { String sample = "VendorID,tpep_pickup_datetime,tpep_dropoff_datetime,passenger_count,trip_distance,RatecodeID," + "store_and_fwd_flag,PULocationID,DOLocationID,payment_type,fare_amount,extra,mta_tax,tip_amount,tolls_amount," + @@ -131,7 +251,8 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { String charset = randomFrom(POSSIBLE_CHARSETS); Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); - FileStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker); + FileStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker, + FileStructureOverrides.EMPTY_OVERRIDES); FileStructure structure = structureFinder.getStructure(); @@ -148,6 +269,7 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { structure.getExcludeLinesPattern()); assertEquals("^.*?,\"?\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}", structure.getMultilineStartPattern()); assertEquals(Character.valueOf(','), structure.getDelimiter()); + assertEquals(Character.valueOf('"'), structure.getQuote()); assertTrue(structure.getHasHeaderRow()); assertNull(structure.getShouldTrimFields()); assertEquals(Arrays.asList("VendorID", "tpep_pickup_datetime", "tpep_dropoff_datetime", "passenger_count", "trip_distance", @@ -158,6 +280,53 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { assertEquals(Collections.singletonList("YYYY-MM-dd HH:mm:ss"), structure.getTimestampFormats()); } + public void testCreateConfigsGivenCsvWithTrailingNullsExceptHeaderAndColumnNamesOverride() throws Exception { + + FileStructureOverrides overrides = FileStructureOverrides.builder() + .setColumnNames(Arrays.asList("my_VendorID", "my_tpep_pickup_datetime", "my_tpep_dropoff_datetime", "my_passenger_count", + "my_trip_distance", "my_RatecodeID", "my_store_and_fwd_flag", "my_PULocationID", "my_DOLocationID", "my_payment_type", + "my_fare_amount", "my_extra", "my_mta_tax", "my_tip_amount", "my_tolls_amount", "my_improvement_surcharge", + "my_total_amount")).build(); + + String sample = "VendorID,tpep_pickup_datetime,tpep_dropoff_datetime,passenger_count,trip_distance,RatecodeID," + + "store_and_fwd_flag,PULocationID,DOLocationID,payment_type,fare_amount,extra,mta_tax,tip_amount,tolls_amount," + + "improvement_surcharge,total_amount\n" + + "2,2016-12-31 15:15:01,2016-12-31 15:15:09,1,.00,1,N,264,264,2,1,0,0.5,0,0,0.3,1.8,,\n" + + "1,2016-12-01 00:00:01,2016-12-01 00:10:22,1,1.60,1,N,163,143,2,9,0.5,0.5,0,0,0.3,10.3,,\n" + + "1,2016-12-01 00:00:01,2016-12-01 00:11:01,1,1.40,1,N,164,229,1,9,0.5,0.5,2.05,0,0.3,12.35,,\n"; + assertTrue(csvFactory.canCreateFromSample(explanation, sample)); + + String charset = randomFrom(POSSIBLE_CHARSETS); + Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); + FileStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker, overrides); + + FileStructure structure = structureFinder.getStructure(); + + assertEquals(FileStructure.Format.DELIMITED, structure.getFormat()); + assertEquals(charset, structure.getCharset()); + if (hasByteOrderMarker == null) { + assertNull(structure.getHasByteOrderMarker()); + } else { + assertEquals(hasByteOrderMarker, structure.getHasByteOrderMarker()); + } + assertEquals("^\"?VendorID\"?,\"?tpep_pickup_datetime\"?,\"?tpep_dropoff_datetime\"?,\"?passenger_count\"?,\"?trip_distance\"?," + + "\"?RatecodeID\"?,\"?store_and_fwd_flag\"?,\"?PULocationID\"?,\"?DOLocationID\"?,\"?payment_type\"?,\"?fare_amount\"?," + + "\"?extra\"?,\"?mta_tax\"?,\"?tip_amount\"?,\"?tolls_amount\"?,\"?improvement_surcharge\"?,\"?total_amount\"?", + structure.getExcludeLinesPattern()); + assertEquals("^.*?,\"?\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}", structure.getMultilineStartPattern()); + assertEquals(Character.valueOf(','), structure.getDelimiter()); + assertEquals(Character.valueOf('"'), structure.getQuote()); + assertTrue(structure.getHasHeaderRow()); + assertNull(structure.getShouldTrimFields()); + assertEquals(Arrays.asList("my_VendorID", "my_tpep_pickup_datetime", "my_tpep_dropoff_datetime", "my_passenger_count", + "my_trip_distance", "my_RatecodeID", "my_store_and_fwd_flag", "my_PULocationID", "my_DOLocationID", "my_payment_type", + "my_fare_amount", "my_extra", "my_mta_tax", "my_tip_amount", "my_tolls_amount", "my_improvement_surcharge", "my_total_amount"), + structure.getColumnNames()); + assertNull(structure.getGrokPattern()); + assertEquals("my_tpep_pickup_datetime", structure.getTimestampField()); + assertEquals(Collections.singletonList("YYYY-MM-dd HH:mm:ss"), structure.getTimestampFormats()); + } + public void testCreateConfigsGivenCsvWithTimeLastColumn() throws Exception { String sample = "\"pos_id\",\"trip_id\",\"latitude\",\"longitude\",\"altitude\",\"timestamp\"\n" + "\"1\",\"3\",\"4703.7815\",\"1527.4713\",\"359.9\",\"2017-01-19 16:19:04.742113\"\n" + @@ -166,7 +335,8 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { String charset = randomFrom(POSSIBLE_CHARSETS); Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); - FileStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker); + FileStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker, + FileStructureOverrides.EMPTY_OVERRIDES); FileStructure structure = structureFinder.getStructure(); @@ -181,6 +351,7 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { structure.getExcludeLinesPattern()); assertNull(structure.getMultilineStartPattern()); assertEquals(Character.valueOf(','), structure.getDelimiter()); + assertEquals(Character.valueOf('"'), structure.getQuote()); assertTrue(structure.getHasHeaderRow()); assertNull(structure.getShouldTrimFields()); assertEquals(Arrays.asList("pos_id", "trip_id", "latitude", "longitude", "altitude", "timestamp"), structure.getColumnNames()); @@ -197,7 +368,7 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { "2014-06-23 00:00:01Z,KLM,1355.4812,farequote\n"; Tuple header = DelimitedFileStructureFinder.findHeaderFromSample(explanation, - DelimitedFileStructureFinder.readRows(withHeader, CsvPreference.EXCEL_PREFERENCE).v1()); + DelimitedFileStructureFinder.readRows(withHeader, CsvPreference.EXCEL_PREFERENCE).v1(), FileStructureOverrides.EMPTY_OVERRIDES); assertTrue(header.v1()); assertThat(header.v2(), arrayContaining("time", "airline", "responsetime", "sourcetype")); @@ -210,7 +381,8 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { "2014-06-23 00:00:01Z,KLM,1355.4812,farequote\n"; Tuple header = DelimitedFileStructureFinder.findHeaderFromSample(explanation, - DelimitedFileStructureFinder.readRows(withoutHeader, CsvPreference.EXCEL_PREFERENCE).v1()); + DelimitedFileStructureFinder.readRows(withoutHeader, CsvPreference.EXCEL_PREFERENCE).v1(), + FileStructureOverrides.EMPTY_OVERRIDES); assertFalse(header.v1()); assertThat(header.v2(), arrayContaining("", "", "", "")); @@ -283,12 +455,12 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { public void testRowContainsDuplicateNonEmptyValues() { - assertFalse(DelimitedFileStructureFinder.rowContainsDuplicateNonEmptyValues(Collections.singletonList("a"))); - assertFalse(DelimitedFileStructureFinder.rowContainsDuplicateNonEmptyValues(Collections.singletonList(""))); - assertFalse(DelimitedFileStructureFinder.rowContainsDuplicateNonEmptyValues(Arrays.asList("a", "b", "c"))); - assertTrue(DelimitedFileStructureFinder.rowContainsDuplicateNonEmptyValues(Arrays.asList("a", "b", "a"))); - assertTrue(DelimitedFileStructureFinder.rowContainsDuplicateNonEmptyValues(Arrays.asList("a", "b", "b"))); - assertFalse(DelimitedFileStructureFinder.rowContainsDuplicateNonEmptyValues(Arrays.asList("a", "", ""))); - assertFalse(DelimitedFileStructureFinder.rowContainsDuplicateNonEmptyValues(Arrays.asList("", "a", ""))); + assertNull(DelimitedFileStructureFinder.findDuplicateNonEmptyValues(Collections.singletonList("a"))); + assertNull(DelimitedFileStructureFinder.findDuplicateNonEmptyValues(Collections.singletonList(""))); + assertNull(DelimitedFileStructureFinder.findDuplicateNonEmptyValues(Arrays.asList("a", "b", "c"))); + assertEquals("a", DelimitedFileStructureFinder.findDuplicateNonEmptyValues(Arrays.asList("a", "b", "a"))); + assertEquals("b", DelimitedFileStructureFinder.findDuplicateNonEmptyValues(Arrays.asList("a", "b", "b"))); + assertNull(DelimitedFileStructureFinder.findDuplicateNonEmptyValues(Arrays.asList("a", "", ""))); + assertNull(DelimitedFileStructureFinder.findDuplicateNonEmptyValues(Arrays.asList("", "a", ""))); } } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureFinderManagerTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureFinderManagerTests.java index 10e780f1d34..00929ff474c 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureFinderManagerTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureFinderManagerTests.java @@ -6,12 +6,14 @@ package org.elasticsearch.xpack.ml.filestructurefinder; import com.ibm.icu.text.CharsetMatch; +import org.elasticsearch.xpack.core.ml.filestructurefinder.FileStructure; import java.io.ByteArrayInputStream; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import static org.elasticsearch.xpack.ml.filestructurefinder.FileStructureOverrides.EMPTY_OVERRIDES; import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.core.IsInstanceOf.instanceOf; @@ -47,26 +49,62 @@ public class FileStructureFinderManagerTests extends FileStructureTestCase { } public void testMakeBestStructureGivenJson() throws Exception { - assertThat(structureFinderManager.makeBestStructureFinder(explanation, - "{ \"time\": \"2018-05-17T13:41:23\", \"message\": \"hello\" }", StandardCharsets.UTF_8.name(), randomBoolean()), - instanceOf(JsonFileStructureFinder.class)); + assertThat(structureFinderManager.makeBestStructureFinder(explanation, JSON_SAMPLE, StandardCharsets.UTF_8.name(), randomBoolean(), + EMPTY_OVERRIDES), instanceOf(JsonFileStructureFinder.class)); + } + + public void testMakeBestStructureGivenJsonAndDelimitedOverride() throws Exception { + + // Need to change the quote character from the default of double quotes + // otherwise the quotes in the JSON will stop it parsing as CSV + FileStructureOverrides overrides = FileStructureOverrides.builder() + .setFormat(FileStructure.Format.DELIMITED).setQuote('\'').build(); + + assertThat(structureFinderManager.makeBestStructureFinder(explanation, JSON_SAMPLE, StandardCharsets.UTF_8.name(), randomBoolean(), + overrides), instanceOf(DelimitedFileStructureFinder.class)); } public void testMakeBestStructureGivenXml() throws Exception { - assertThat(structureFinderManager.makeBestStructureFinder(explanation, - "hello", StandardCharsets.UTF_8.name(), randomBoolean()), - instanceOf(XmlFileStructureFinder.class)); + assertThat(structureFinderManager.makeBestStructureFinder(explanation, XML_SAMPLE, StandardCharsets.UTF_8.name(), randomBoolean(), + EMPTY_OVERRIDES), instanceOf(XmlFileStructureFinder.class)); + } + + public void testMakeBestStructureGivenXmlAndTextOverride() throws Exception { + + FileStructureOverrides overrides = FileStructureOverrides.builder().setFormat(FileStructure.Format.SEMI_STRUCTURED_TEXT).build(); + + assertThat(structureFinderManager.makeBestStructureFinder(explanation, XML_SAMPLE, StandardCharsets.UTF_8.name(), randomBoolean(), + overrides), instanceOf(TextLogFileStructureFinder.class)); } public void testMakeBestStructureGivenCsv() throws Exception { - assertThat(structureFinderManager.makeBestStructureFinder(explanation, "time,message\n" + - "2018-05-17T13:41:23,hello\n", StandardCharsets.UTF_8.name(), randomBoolean()), - instanceOf(DelimitedFileStructureFinder.class)); + assertThat(structureFinderManager.makeBestStructureFinder(explanation, CSV_SAMPLE, StandardCharsets.UTF_8.name(), randomBoolean(), + EMPTY_OVERRIDES), instanceOf(DelimitedFileStructureFinder.class)); + } + + public void testMakeBestStructureGivenCsvAndJsonOverride() { + + FileStructureOverrides overrides = FileStructureOverrides.builder().setFormat(FileStructure.Format.JSON).build(); + + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> structureFinderManager.makeBestStructureFinder(explanation, CSV_SAMPLE, StandardCharsets.UTF_8.name(), randomBoolean(), + overrides)); + + assertEquals("Input did not match the specified format [json]", e.getMessage()); } public void testMakeBestStructureGivenText() throws Exception { - assertThat(structureFinderManager.makeBestStructureFinder(explanation, "[2018-05-17T13:41:23] hello\n" + - "[2018-05-17T13:41:24] hello again\n", StandardCharsets.UTF_8.name(), randomBoolean()), - instanceOf(TextLogFileStructureFinder.class)); + assertThat(structureFinderManager.makeBestStructureFinder(explanation, TEXT_SAMPLE, StandardCharsets.UTF_8.name(), randomBoolean(), + EMPTY_OVERRIDES), instanceOf(TextLogFileStructureFinder.class)); + } + + public void testMakeBestStructureGivenTextAndDelimitedOverride() throws Exception { + + // Every line of the text sample has two colons, so colon delimited is possible, just very weird + FileStructureOverrides overrides = FileStructureOverrides.builder() + .setFormat(FileStructure.Format.DELIMITED).setDelimiter(':').build(); + + assertThat(structureFinderManager.makeBestStructureFinder(explanation, TEXT_SAMPLE, StandardCharsets.UTF_8.name(), randomBoolean(), + overrides), instanceOf(DelimitedFileStructureFinder.class)); } } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureUtilsTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureUtilsTests.java index ac8f95670ab..8dbfb6a8047 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureUtilsTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureUtilsTests.java @@ -17,6 +17,7 @@ import java.util.List; import java.util.Map; import java.util.SortedMap; +import static org.elasticsearch.xpack.ml.filestructurefinder.FileStructureOverrides.EMPTY_OVERRIDES; import static org.hamcrest.Matchers.contains; public class FileStructureUtilsTests extends FileStructureTestCase { @@ -32,57 +33,106 @@ public class FileStructureUtilsTests extends FileStructureTestCase { assertFalse(FileStructureUtils.isMoreLikelyTextThanKeyword(randomAlphaOfLengthBetween(1, 256))); } - public void testSingleSampleSingleField() { + public void testGuessTimestampGivenSingleSampleSingleField() { Map sample = Collections.singletonMap("field1", "2018-05-24T17:28:31,735"); Tuple match = - FileStructureUtils.guessTimestampField(explanation, Collections.singletonList(sample)); + FileStructureUtils.guessTimestampField(explanation, Collections.singletonList(sample), EMPTY_OVERRIDES); assertNotNull(match); assertEquals("field1", match.v1()); assertThat(match.v2().dateFormats, contains("ISO8601")); assertEquals("TIMESTAMP_ISO8601", match.v2().grokPatternName); } - public void testSamplesWithSameSingleTimeField() { + public void testGuessTimestampGivenSingleSampleSingleFieldAndConsistentTimeFieldOverride() { + + FileStructureOverrides overrides = FileStructureOverrides.builder().setTimestampField("field1").build(); + + Map sample = Collections.singletonMap("field1", "2018-05-24T17:28:31,735"); + Tuple match = + FileStructureUtils.guessTimestampField(explanation, Collections.singletonList(sample), overrides); + assertNotNull(match); + assertEquals("field1", match.v1()); + assertThat(match.v2().dateFormats, contains("ISO8601")); + assertEquals("TIMESTAMP_ISO8601", match.v2().grokPatternName); + } + + public void testGuessTimestampGivenSingleSampleSingleFieldAndImpossibleTimeFieldOverride() { + + FileStructureOverrides overrides = FileStructureOverrides.builder().setTimestampField("field2").build(); + + Map sample = Collections.singletonMap("field1", "2018-05-24T17:28:31,735"); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> FileStructureUtils.guessTimestampField(explanation, Collections.singletonList(sample), overrides)); + + assertEquals("Specified timestamp field [field2] is not present in record [{field1=2018-05-24T17:28:31,735}]", e.getMessage()); + } + + public void testGuessTimestampGivenSingleSampleSingleFieldAndConsistentTimeFormatOverride() { + + FileStructureOverrides overrides = FileStructureOverrides.builder().setTimestampFormat("ISO8601").build(); + + Map sample = Collections.singletonMap("field1", "2018-05-24T17:28:31,735"); + Tuple match = + FileStructureUtils.guessTimestampField(explanation, Collections.singletonList(sample), overrides); + assertNotNull(match); + assertEquals("field1", match.v1()); + assertThat(match.v2().dateFormats, contains("ISO8601")); + assertEquals("TIMESTAMP_ISO8601", match.v2().grokPatternName); + } + + public void testGuessTimestampGivenSingleSampleSingleFieldAndImpossibleTimeFormatOverride() { + + FileStructureOverrides overrides = FileStructureOverrides.builder().setTimestampFormat("EEE MMM dd HH:mm:ss YYYY").build(); + + Map sample = Collections.singletonMap("field1", "2018-05-24T17:28:31,735"); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> FileStructureUtils.guessTimestampField(explanation, Collections.singletonList(sample), overrides)); + + assertEquals("Specified timestamp format [EEE MMM dd HH:mm:ss YYYY] does not match for record [{field1=2018-05-24T17:28:31,735}]", + e.getMessage()); + } + + public void testGuessTimestampGivenSamplesWithSameSingleTimeField() { Map sample1 = Collections.singletonMap("field1", "2018-05-24T17:28:31,735"); Map sample2 = Collections.singletonMap("field1", "2018-05-24T17:33:39,406"); Tuple match = - FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2)); + FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2), EMPTY_OVERRIDES); assertNotNull(match); assertEquals("field1", match.v1()); assertThat(match.v2().dateFormats, contains("ISO8601")); assertEquals("TIMESTAMP_ISO8601", match.v2().grokPatternName); } - public void testSamplesWithOneSingleTimeFieldDifferentFormat() { + public void testGuessTimestampGivenSamplesWithOneSingleTimeFieldDifferentFormat() { Map sample1 = Collections.singletonMap("field1", "2018-05-24T17:28:31,735"); Map sample2 = Collections.singletonMap("field1", "2018-05-24 17:33:39,406"); Tuple match = - FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2)); + FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2), EMPTY_OVERRIDES); assertNull(match); } - public void testSamplesWithDifferentSingleTimeField() { + public void testGuessTimestampGivenSamplesWithDifferentSingleTimeField() { Map sample1 = Collections.singletonMap("field1", "2018-05-24T17:28:31,735"); Map sample2 = Collections.singletonMap("another_field", "2018-05-24T17:33:39,406"); Tuple match = - FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2)); + FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2), EMPTY_OVERRIDES); assertNull(match); } - public void testSingleSampleManyFieldsOneTimeFormat() { + public void testGuessTimestampGivenSingleSampleManyFieldsOneTimeFormat() { Map sample = new LinkedHashMap<>(); sample.put("foo", "not a time"); sample.put("time", "2018-05-24 17:28:31,735"); sample.put("bar", 42); Tuple match = - FileStructureUtils.guessTimestampField(explanation, Collections.singletonList(sample)); + FileStructureUtils.guessTimestampField(explanation, Collections.singletonList(sample), EMPTY_OVERRIDES); assertNotNull(match); assertEquals("time", match.v1()); assertThat(match.v2().dateFormats, contains("YYYY-MM-dd HH:mm:ss,SSS")); assertEquals("TIMESTAMP_ISO8601", match.v2().grokPatternName); } - public void testSamplesWithManyFieldsSameSingleTimeFormat() { + public void testGuessTimestampGivenSamplesWithManyFieldsSameSingleTimeFormat() { Map sample1 = new LinkedHashMap<>(); sample1.put("foo", "not a time"); sample1.put("time", "2018-05-24 17:28:31,735"); @@ -92,14 +142,14 @@ public class FileStructureUtilsTests extends FileStructureTestCase { sample2.put("time", "2018-05-29 11:53:02,837"); sample2.put("bar", 17); Tuple match = - FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2)); + FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2), EMPTY_OVERRIDES); assertNotNull(match); assertEquals("time", match.v1()); assertThat(match.v2().dateFormats, contains("YYYY-MM-dd HH:mm:ss,SSS")); assertEquals("TIMESTAMP_ISO8601", match.v2().grokPatternName); } - public void testSamplesWithManyFieldsSameTimeFieldDifferentTimeFormat() { + public void testGuessTimestampGivenSamplesWithManyFieldsSameTimeFieldDifferentTimeFormat() { Map sample1 = new LinkedHashMap<>(); sample1.put("foo", "not a time"); sample1.put("time", "2018-05-24 17:28:31,735"); @@ -109,11 +159,11 @@ public class FileStructureUtilsTests extends FileStructureTestCase { sample2.put("time", "May 29 2018 11:53:02"); sample2.put("bar", 17); Tuple match = - FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2)); + FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2), EMPTY_OVERRIDES); assertNull(match); } - public void testSamplesWithManyFieldsSameSingleTimeFormatDistractionBefore() { + public void testGuessTimestampGivenSamplesWithManyFieldsSameSingleTimeFormatDistractionBefore() { Map sample1 = new LinkedHashMap<>(); sample1.put("red_herring", "May 29 2007 11:53:02"); sample1.put("time", "2018-05-24 17:28:31,735"); @@ -123,14 +173,14 @@ public class FileStructureUtilsTests extends FileStructureTestCase { sample2.put("time", "2018-05-29 11:53:02,837"); sample2.put("bar", 17); Tuple match = - FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2)); + FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2), EMPTY_OVERRIDES); assertNotNull(match); assertEquals("time", match.v1()); assertThat(match.v2().dateFormats, contains("YYYY-MM-dd HH:mm:ss,SSS")); assertEquals("TIMESTAMP_ISO8601", match.v2().grokPatternName); } - public void testSamplesWithManyFieldsSameSingleTimeFormatDistractionAfter() { + public void testGuessTimestampGivenSamplesWithManyFieldsSameSingleTimeFormatDistractionAfter() { Map sample1 = new LinkedHashMap<>(); sample1.put("foo", "not a time"); sample1.put("time", "May 24 2018 17:28:31"); @@ -140,14 +190,14 @@ public class FileStructureUtilsTests extends FileStructureTestCase { sample2.put("time", "May 29 2018 11:53:02"); sample2.put("red_herring", "17"); Tuple match = - FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2)); + FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2), EMPTY_OVERRIDES); assertNotNull(match); assertEquals("time", match.v1()); assertThat(match.v2().dateFormats, contains("MMM dd YYYY HH:mm:ss", "MMM d YYYY HH:mm:ss")); assertEquals("CISCOTIMESTAMP", match.v2().grokPatternName); } - public void testSamplesWithManyFieldsInconsistentTimeFields() { + public void testGuessTimestampGivenSamplesWithManyFieldsInconsistentTimeFields() { Map sample1 = new LinkedHashMap<>(); sample1.put("foo", "not a time"); sample1.put("time1", "May 24 2018 17:28:31"); @@ -157,11 +207,11 @@ public class FileStructureUtilsTests extends FileStructureTestCase { sample2.put("time2", "May 29 2018 11:53:02"); sample2.put("bar", 42); Tuple match = - FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2)); + FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2), EMPTY_OVERRIDES); assertNull(match); } - public void testSamplesWithManyFieldsInconsistentAndConsistentTimeFields() { + public void testGuessTimestampGivenSamplesWithManyFieldsInconsistentAndConsistentTimeFields() { Map sample1 = new LinkedHashMap<>(); sample1.put("foo", "not a time"); sample1.put("time1", "2018-05-09 17:28:31,735"); @@ -173,7 +223,7 @@ public class FileStructureUtilsTests extends FileStructureTestCase { sample2.put("time3", "Thu, May 10 2018 11:53:02"); sample2.put("bar", 42); Tuple match = - FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2)); + FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2), EMPTY_OVERRIDES); assertNotNull(match); assertEquals("time2", match.v1()); assertThat(match.v2().dateFormats, contains("MMM dd YYYY HH:mm:ss", "MMM d YYYY HH:mm:ss")); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/GrokPatternCreatorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/GrokPatternCreatorTests.java index 858709e2764..271e071fc27 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/GrokPatternCreatorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/GrokPatternCreatorTests.java @@ -244,8 +244,7 @@ public class GrokPatternCreatorTests extends FileStructureTestCase { grokPatternCreator.createGrokPatternFromExamples("TIMESTAMP_ISO8601", "timestamp")); assertEquals(5, mappings.size()); assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "long"), mappings.get("field")); - assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "date"), - mappings.get("extra_timestamp")); + assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "date"), mappings.get("extra_timestamp")); assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "long"), mappings.get("field2")); assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "ip"), mappings.get("ipaddress")); assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "keyword"), mappings.get("loglevel")); @@ -273,7 +272,8 @@ public class GrokPatternCreatorTests extends FileStructureTestCase { Map mappings = new HashMap<>(); GrokPatternCreator grokPatternCreator = new GrokPatternCreator(explanation, sampleMessages, mappings, null); - assertEquals(new Tuple<>("timestamp", "%{COMBINEDAPACHELOG}"), grokPatternCreator.findFullLineGrokPattern()); + assertEquals(new Tuple<>("timestamp", "%{COMBINEDAPACHELOG}"), + grokPatternCreator.findFullLineGrokPattern(randomBoolean() ? "timestamp" : null)); assertEquals(10, mappings.size()); assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "text"), mappings.get("agent")); assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "keyword"), mappings.get("auth")); @@ -323,4 +323,59 @@ public class GrokPatternCreatorTests extends FileStructureTestCase { assertEquals("", grokPatternCreator.getOverallGrokPatternBuilder().toString()); assertSame(snippets, adjustedSnippets); } + + public void testValidateFullLineGrokPatternGivenValid() { + + String timestampField = "utc_timestamp"; + String grokPattern = "%{INT:serial_no}\\t%{TIMESTAMP_ISO8601:local_timestamp}\\t%{TIMESTAMP_ISO8601:utc_timestamp}\\t" + + "%{INT:user_id}\\t%{HOSTNAME:host}\\t%{IP:client_ip}\\t%{WORD:method}\\t%{LOGLEVEL:severity}\\t%{PROG:program}\\t" + + "%{GREEDYDATA:message}"; + + // Two timestamps: one local, one UTC + Collection sampleMessages = Arrays.asList( + "559550912540598297\t2016-04-20T14:06:53\t2016-04-20T21:06:53Z\t38545844\tserv02nw07\t192.168.114.28\tAuthpriv\t" + + "Info\tsshd\tsubsystem request for sftp", + "559550912548986880\t2016-04-20T14:06:53\t2016-04-20T21:06:53Z\t9049724\tserv02nw03\t10.120.48.147\tAuthpriv\t" + + "Info\tsshd\tsubsystem request for sftp", + "559550912548986887\t2016-04-20T14:06:53\t2016-04-20T21:06:53Z\t884343\tserv02tw03\t192.168.121.189\tAuthpriv\t" + + "Info\tsshd\tsubsystem request for sftp", + "559550912603512850\t2016-04-20T14:06:53\t2016-04-20T21:06:53Z\t8907014\tserv02nw01\t192.168.118.208\tAuthpriv\t" + + "Info\tsshd\tsubsystem request for sftp"); + + Map mappings = new HashMap<>(); + GrokPatternCreator grokPatternCreator = new GrokPatternCreator(explanation, sampleMessages, mappings, null); + + grokPatternCreator.validateFullLineGrokPattern(grokPattern, timestampField); + assertEquals(9, mappings.size()); + assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "long"), mappings.get("serial_no")); + assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "date"), mappings.get("local_timestamp")); + assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "long"), mappings.get("user_id")); + assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "keyword"), mappings.get("host")); + assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "ip"), mappings.get("client_ip")); + assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "keyword"), mappings.get("method")); + assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "keyword"), mappings.get("program")); + assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "keyword"), mappings.get("message")); + } + + public void testValidateFullLineGrokPatternGivenInvalid() { + + String timestampField = "utc_timestamp"; + String grokPattern = "%{INT:serial_no}\\t%{TIMESTAMP_ISO8601:local_timestamp}\\t%{TIMESTAMP_ISO8601:utc_timestamp}\\t" + + "%{INT:user_id}\\t%{HOSTNAME:host}\\t%{IP:client_ip}\\t%{WORD:method}\\t%{LOGLEVEL:severity}\\t%{PROG:program}\\t" + + "%{GREEDYDATA:message}"; + + Collection sampleMessages = Arrays.asList( + "Sep 8 11:55:06 linux named[22529]: error (unexpected RCODE REFUSED) resolving 'elastic.slack.com/A/IN': 95.110.64.205#53", + "Sep 8 11:55:08 linux named[22529]: error (unexpected RCODE REFUSED) resolving 'slack-imgs.com/A/IN': 95.110.64.205#53", + "Sep 8 11:55:35 linux named[22529]: error (unexpected RCODE REFUSED) resolving 'www.elastic.co/A/IN': 95.110.68.206#53", + "Sep 8 11:55:42 linux named[22529]: error (unexpected RCODE REFUSED) resolving 'b.akamaiedge.net/A/IN': 95.110.64.205#53"); + + Map mappings = new HashMap<>(); + GrokPatternCreator grokPatternCreator = new GrokPatternCreator(explanation, sampleMessages, mappings, null); + + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> grokPatternCreator.validateFullLineGrokPattern(grokPattern, timestampField)); + + assertEquals("Supplied Grok pattern [" + grokPattern + "] does not match sample messages", e.getMessage()); + } } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/JsonFileStructureFinderTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/JsonFileStructureFinderTests.java index f41868be862..6856e9a6021 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/JsonFileStructureFinderTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/JsonFileStructureFinderTests.java @@ -18,7 +18,8 @@ public class JsonFileStructureFinderTests extends FileStructureTestCase { String charset = randomFrom(POSSIBLE_CHARSETS); Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); - FileStructureFinder structureFinder = factory.createFromSample(explanation, JSON_SAMPLE, charset, hasByteOrderMarker); + FileStructureFinder structureFinder = factory.createFromSample(explanation, JSON_SAMPLE, charset, hasByteOrderMarker, + FileStructureOverrides.EMPTY_OVERRIDES); FileStructure structure = structureFinder.getStructure(); @@ -32,6 +33,7 @@ public class JsonFileStructureFinderTests extends FileStructureTestCase { assertNull(structure.getExcludeLinesPattern()); assertNull(structure.getMultilineStartPattern()); assertNull(structure.getDelimiter()); + assertNull(structure.getQuote()); assertNull(structure.getHasHeaderRow()); assertNull(structure.getShouldTrimFields()); assertNull(structure.getGrokPattern()); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/TextLogFileStructureFinderTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/TextLogFileStructureFinderTests.java index a23080a8272..5bc40a16511 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/TextLogFileStructureFinderTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/TextLogFileStructureFinderTests.java @@ -15,6 +15,90 @@ import java.util.Set; public class TextLogFileStructureFinderTests extends FileStructureTestCase { + private static final String EXCEPTION_TRACE_SAMPLE = + "[2018-02-28T14:49:40,517][DEBUG][o.e.a.b.TransportShardBulkAction] [an_index][2] failed to execute bulk item " + + "(index) BulkShardRequest [[an_index][2]] containing [33] requests\n" + + "java.lang.IllegalArgumentException: Document contains at least one immense term in field=\"message.keyword\" (whose UTF8 " + + "encoding is longer than the max length 32766), all of which were skipped. Please correct the analyzer to not produce " + + "such terms. The prefix of the first immense term is: '[60, 83, 79, 65, 80, 45, 69, 78, 86, 58, 69, 110, 118, 101, 108, " + + "111, 112, 101, 32, 120, 109, 108, 110, 115, 58, 83, 79, 65, 80, 45]...', original message: bytes can be at most 32766 " + + "in length; got 49023\n" + + "\tat org.apache.lucene.index.DefaultIndexingChain$PerField.invert(DefaultIndexingChain.java:796) " + + "~[lucene-core-7.2.1.jar:7.2.1 b2b6438b37073bee1fca40374e85bf91aa457c0b - ubuntu - 2018-01-10 00:48:43]\n" + + "\tat org.apache.lucene.index.DefaultIndexingChain.processField(DefaultIndexingChain.java:430) " + + "~[lucene-core-7.2.1.jar:7.2.1 b2b6438b37073bee1fca40374e85bf91aa457c0b - ubuntu - 2018-01-10 00:48:43]\n" + + "\tat org.apache.lucene.index.DefaultIndexingChain.processDocument(DefaultIndexingChain.java:392) " + + "~[lucene-core-7.2.1.jar:7.2.1 b2b6438b37073bee1fca40374e85bf91aa457c0b - ubuntu - 2018-01-10 00:48:43]\n" + + "\tat org.apache.lucene.index.DocumentsWriterPerThread.updateDocument(DocumentsWriterPerThread.java:240) " + + "~[lucene-core-7.2.1.jar:7.2.1 b2b6438b37073bee1fca40374e85bf91aa457c0b - ubuntu - 2018-01-10 00:48:43]\n" + + "\tat org.apache.lucene.index.DocumentsWriter.updateDocument(DocumentsWriter.java:496) " + + "~[lucene-core-7.2.1.jar:7.2.1 b2b6438b37073bee1fca40374e85bf91aa457c0b - ubuntu - 2018-01-10 00:48:43]\n" + + "\tat org.apache.lucene.index.IndexWriter.updateDocument(IndexWriter.java:1729) " + + "~[lucene-core-7.2.1.jar:7.2.1 b2b6438b37073bee1fca40374e85bf91aa457c0b - ubuntu - 2018-01-10 00:48:43]\n" + + "\tat org.apache.lucene.index.IndexWriter.addDocument(IndexWriter.java:1464) " + + "~[lucene-core-7.2.1.jar:7.2.1 b2b6438b37073bee1fca40374e85bf91aa457c0b - ubuntu - 2018-01-10 00:48:43]\n" + + "\tat org.elasticsearch.index.engine.InternalEngine.index(InternalEngine.java:1070) ~[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.index.engine.InternalEngine.indexIntoLucene(InternalEngine.java:1012) " + + "~[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.index.engine.InternalEngine.index(InternalEngine.java:878) ~[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.index.shard.IndexShard.index(IndexShard.java:738) ~[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.index.shard.IndexShard.applyIndexOperation(IndexShard.java:707) ~[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.index.shard.IndexShard.applyIndexOperationOnPrimary(IndexShard.java:673) " + + "~[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.bulk.TransportShardBulkAction.executeIndexRequestOnPrimary(TransportShardBulkAction.java:548) " + + "~[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.bulk.TransportShardBulkAction.executeIndexRequest(TransportShardBulkAction.java:140) " + + "[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.bulk.TransportShardBulkAction.executeBulkItemRequest(TransportShardBulkAction.java:236) " + + "[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.bulk.TransportShardBulkAction.performOnPrimary(TransportShardBulkAction.java:123) " + + "[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.bulk.TransportShardBulkAction.shardOperationOnPrimary(TransportShardBulkAction.java:110) " + + "[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.bulk.TransportShardBulkAction.shardOperationOnPrimary(TransportShardBulkAction.java:72) " + + "[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$PrimaryShardReference.perform" + + "(TransportReplicationAction.java:1034) [elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$PrimaryShardReference.perform" + + "(TransportReplicationAction.java:1012) [elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.support.replication.ReplicationOperation.execute(ReplicationOperation.java:103) " + + "[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$AsyncPrimaryAction.onResponse" + + "(TransportReplicationAction.java:359) [elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$AsyncPrimaryAction.onResponse" + + "(TransportReplicationAction.java:299) [elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$1.onResponse" + + "(TransportReplicationAction.java:975) [elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$1.onResponse" + + "(TransportReplicationAction.java:972) [elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.index.shard.IndexShardOperationPermits.acquire(IndexShardOperationPermits.java:238) " + + "[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.index.shard.IndexShard.acquirePrimaryOperationPermit(IndexShard.java:2220) " + + "[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.support.replication.TransportReplicationAction.acquirePrimaryShardReference" + + "(TransportReplicationAction.java:984) [elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.support.replication.TransportReplicationAction.access$500(TransportReplicationAction.java:98) " + + "[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$AsyncPrimaryAction.doRun" + + "(TransportReplicationAction.java:320) [elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.common.util.concurrent.AbstractRunnable.run(AbstractRunnable.java:37) " + + "[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$PrimaryOperationTransportHandler" + + ".messageReceived(TransportReplicationAction.java:295) [elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$PrimaryOperationTransportHandler" + + ".messageReceived(TransportReplicationAction.java:282) [elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.transport.RequestHandlerRegistry.processMessageReceived(RequestHandlerRegistry.java:66) " + + "[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.transport.TransportService$7.doRun(TransportService.java:656) " + + "[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.common.util.concurrent.ThreadContext$ContextPreservingAbstractRunnable.doRun(ThreadContext.java:635) " + + "[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.common.util.concurrent.AbstractRunnable.run(AbstractRunnable.java:37) " + + "[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [?:1.8.0_144]\n" + + "\tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [?:1.8.0_144]\n" + + "\tat java.lang.Thread.run(Thread.java:748) [?:1.8.0_144]\n"; + private FileStructureFinderFactory factory = new TextLogFileStructureFinderFactory(); public void testCreateConfigsGivenElasticsearchLog() throws Exception { @@ -22,7 +106,8 @@ public class TextLogFileStructureFinderTests extends FileStructureTestCase { String charset = randomFrom(POSSIBLE_CHARSETS); Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); - FileStructureFinder structureFinder = factory.createFromSample(explanation, TEXT_SAMPLE, charset, hasByteOrderMarker); + FileStructureFinder structureFinder = factory.createFromSample(explanation, TEXT_SAMPLE, charset, hasByteOrderMarker, + FileStructureOverrides.EMPTY_OVERRIDES); FileStructure structure = structureFinder.getStructure(); @@ -36,6 +121,7 @@ public class TextLogFileStructureFinderTests extends FileStructureTestCase { assertNull(structure.getExcludeLinesPattern()); assertEquals("^\\[\\b\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}", structure.getMultilineStartPattern()); assertNull(structure.getDelimiter()); + assertNull(structure.getQuote()); assertNull(structure.getHasHeaderRow()); assertNull(structure.getShouldTrimFields()); assertEquals("\\[%{TIMESTAMP_ISO8601:timestamp}\\]\\[%{LOGLEVEL:loglevel} \\]\\[.*", structure.getGrokPattern()); @@ -43,6 +129,85 @@ public class TextLogFileStructureFinderTests extends FileStructureTestCase { assertEquals(Collections.singletonList("ISO8601"), structure.getTimestampFormats()); } + public void testCreateConfigsGivenElasticsearchLogAndTimestampFieldOverride() throws Exception { + + FileStructureOverrides overrides = FileStructureOverrides.builder().setTimestampField("my_time").build(); + + assertTrue(factory.canCreateFromSample(explanation, TEXT_SAMPLE)); + + String charset = randomFrom(POSSIBLE_CHARSETS); + Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); + FileStructureFinder structureFinder = factory.createFromSample(explanation, TEXT_SAMPLE, charset, hasByteOrderMarker, overrides); + + FileStructure structure = structureFinder.getStructure(); + + assertEquals(FileStructure.Format.SEMI_STRUCTURED_TEXT, structure.getFormat()); + assertEquals(charset, structure.getCharset()); + if (hasByteOrderMarker == null) { + assertNull(structure.getHasByteOrderMarker()); + } else { + assertEquals(hasByteOrderMarker, structure.getHasByteOrderMarker()); + } + assertNull(structure.getExcludeLinesPattern()); + assertEquals("^\\[\\b\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}", structure.getMultilineStartPattern()); + assertNull(structure.getDelimiter()); + assertNull(structure.getQuote()); + assertNull(structure.getHasHeaderRow()); + assertNull(structure.getShouldTrimFields()); + assertEquals("\\[%{TIMESTAMP_ISO8601:my_time}\\]\\[%{LOGLEVEL:loglevel} \\]\\[.*", structure.getGrokPattern()); + assertEquals("my_time", structure.getTimestampField()); + assertEquals(Collections.singletonList("ISO8601"), structure.getTimestampFormats()); + } + + public void testCreateConfigsGivenElasticsearchLogAndGrokPatternOverride() throws Exception { + + FileStructureOverrides overrides = FileStructureOverrides.builder().setGrokPattern("\\[%{TIMESTAMP_ISO8601:timestamp}\\]" + + "\\[%{LOGLEVEL:loglevel} *\\]\\[%{JAVACLASS:class} *\\] \\[%{HOSTNAME:node}\\] %{JAVALOGMESSAGE:message}").build(); + + assertTrue(factory.canCreateFromSample(explanation, TEXT_SAMPLE)); + + String charset = randomFrom(POSSIBLE_CHARSETS); + Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); + FileStructureFinder structureFinder = factory.createFromSample(explanation, TEXT_SAMPLE, charset, hasByteOrderMarker, overrides); + + FileStructure structure = structureFinder.getStructure(); + + assertEquals(FileStructure.Format.SEMI_STRUCTURED_TEXT, structure.getFormat()); + assertEquals(charset, structure.getCharset()); + if (hasByteOrderMarker == null) { + assertNull(structure.getHasByteOrderMarker()); + } else { + assertEquals(hasByteOrderMarker, structure.getHasByteOrderMarker()); + } + assertNull(structure.getExcludeLinesPattern()); + assertEquals("^\\[\\b\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}", structure.getMultilineStartPattern()); + assertNull(structure.getDelimiter()); + assertNull(structure.getQuote()); + assertNull(structure.getHasHeaderRow()); + assertNull(structure.getShouldTrimFields()); + assertEquals("\\[%{TIMESTAMP_ISO8601:timestamp}\\]\\[%{LOGLEVEL:loglevel} *\\]" + + "\\[%{JAVACLASS:class} *\\] \\[%{HOSTNAME:node}\\] %{JAVALOGMESSAGE:message}", structure.getGrokPattern()); + assertEquals("timestamp", structure.getTimestampField()); + assertEquals(Collections.singletonList("ISO8601"), structure.getTimestampFormats()); + } + + public void testCreateConfigsGivenElasticsearchLogAndImpossibleGrokPatternOverride() { + + // This Grok pattern cannot be matched against the messages in the sample because the fields are in the wrong order + FileStructureOverrides overrides = FileStructureOverrides.builder().setGrokPattern("\\[%{LOGLEVEL:loglevel} *\\]" + + "\\[%{HOSTNAME:node}\\]\\[%{TIMESTAMP_ISO8601:timestamp}\\] \\[%{JAVACLASS:class} *\\] %{JAVALOGMESSAGE:message}").build(); + + assertTrue(factory.canCreateFromSample(explanation, TEXT_SAMPLE)); + + String charset = randomFrom(POSSIBLE_CHARSETS); + Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> factory.createFromSample(explanation, TEXT_SAMPLE, charset, hasByteOrderMarker, overrides)); + + assertEquals("Supplied Grok pattern [\\[%{LOGLEVEL:loglevel} *\\]\\[%{HOSTNAME:node}\\]\\[%{TIMESTAMP_ISO8601:timestamp}\\] " + + "\\[%{JAVACLASS:class} *\\] %{JAVALOGMESSAGE:message}] does not match sample messages", e.getMessage()); + } + public void testCreateMultiLineMessageStartRegexGivenNoPrefaces() { for (TimestampFormatFinder.CandidateTimestampFormat candidateTimestampFormat : TimestampFormatFinder.ORDERED_CANDIDATE_FORMATS) { String simpleDateRegex = candidateTimestampFormat.simplePattern.pattern(); @@ -144,97 +309,17 @@ public class TextLogFileStructureFinderTests extends FileStructureTestCase { "[2018-06-27T11:59:23,588][INFO ][o.e.p.PluginsService ] [node-0] loaded module [x-pack-watcher]\n" + "[2018-06-27T11:59:23,588][INFO ][o.e.p.PluginsService ] [node-0] no plugins loaded\n"; - Tuple> mostLikelyMatch = TextLogFileStructureFinder.mostLikelyTimestamp(sample.split("\n")); + Tuple> mostLikelyMatch = + TextLogFileStructureFinder.mostLikelyTimestamp(sample.split("\n"), FileStructureOverrides.EMPTY_OVERRIDES); assertNotNull(mostLikelyMatch); assertEquals(new TimestampMatch(7, "", "ISO8601", "\\b\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}", "TIMESTAMP_ISO8601", ""), mostLikelyMatch.v1()); } public void testMostLikelyTimestampGivenExceptionTrace() { - String sample = "[2018-02-28T14:49:40,517][DEBUG][o.e.a.b.TransportShardBulkAction] [an_index][2] failed to execute bulk item " + - "(index) BulkShardRequest [[an_index][2]] containing [33] requests\n" + - "java.lang.IllegalArgumentException: Document contains at least one immense term in field=\"message.keyword\" (whose UTF8 " + - "encoding is longer than the max length 32766), all of which were skipped. Please correct the analyzer to not produce " + - "such terms. The prefix of the first immense term is: '[60, 83, 79, 65, 80, 45, 69, 78, 86, 58, 69, 110, 118, 101, 108, " + - "111, 112, 101, 32, 120, 109, 108, 110, 115, 58, 83, 79, 65, 80, 45]...', original message: bytes can be at most 32766 " + - "in length; got 49023\n" + - "\tat org.apache.lucene.index.DefaultIndexingChain$PerField.invert(DefaultIndexingChain.java:796) " + - "~[lucene-core-7.2.1.jar:7.2.1 b2b6438b37073bee1fca40374e85bf91aa457c0b - ubuntu - 2018-01-10 00:48:43]\n" + - "\tat org.apache.lucene.index.DefaultIndexingChain.processField(DefaultIndexingChain.java:430) " + - "~[lucene-core-7.2.1.jar:7.2.1 b2b6438b37073bee1fca40374e85bf91aa457c0b - ubuntu - 2018-01-10 00:48:43]\n" + - "\tat org.apache.lucene.index.DefaultIndexingChain.processDocument(DefaultIndexingChain.java:392) " + - "~[lucene-core-7.2.1.jar:7.2.1 b2b6438b37073bee1fca40374e85bf91aa457c0b - ubuntu - 2018-01-10 00:48:43]\n" + - "\tat org.apache.lucene.index.DocumentsWriterPerThread.updateDocument(DocumentsWriterPerThread.java:240) " + - "~[lucene-core-7.2.1.jar:7.2.1 b2b6438b37073bee1fca40374e85bf91aa457c0b - ubuntu - 2018-01-10 00:48:43]\n" + - "\tat org.apache.lucene.index.DocumentsWriter.updateDocument(DocumentsWriter.java:496) " + - "~[lucene-core-7.2.1.jar:7.2.1 b2b6438b37073bee1fca40374e85bf91aa457c0b - ubuntu - 2018-01-10 00:48:43]\n" + - "\tat org.apache.lucene.index.IndexWriter.updateDocument(IndexWriter.java:1729) " + - "~[lucene-core-7.2.1.jar:7.2.1 b2b6438b37073bee1fca40374e85bf91aa457c0b - ubuntu - 2018-01-10 00:48:43]\n" + - "\tat org.apache.lucene.index.IndexWriter.addDocument(IndexWriter.java:1464) " + - "~[lucene-core-7.2.1.jar:7.2.1 b2b6438b37073bee1fca40374e85bf91aa457c0b - ubuntu - 2018-01-10 00:48:43]\n" + - "\tat org.elasticsearch.index.engine.InternalEngine.index(InternalEngine.java:1070) ~[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.index.engine.InternalEngine.indexIntoLucene(InternalEngine.java:1012) " + - "~[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.index.engine.InternalEngine.index(InternalEngine.java:878) ~[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.index.shard.IndexShard.index(IndexShard.java:738) ~[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.index.shard.IndexShard.applyIndexOperation(IndexShard.java:707) ~[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.index.shard.IndexShard.applyIndexOperationOnPrimary(IndexShard.java:673) " + - "~[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.bulk.TransportShardBulkAction.executeIndexRequestOnPrimary(TransportShardBulkAction.java:548) " + - "~[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.bulk.TransportShardBulkAction.executeIndexRequest(TransportShardBulkAction.java:140) " + - "[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.bulk.TransportShardBulkAction.executeBulkItemRequest(TransportShardBulkAction.java:236) " + - "[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.bulk.TransportShardBulkAction.performOnPrimary(TransportShardBulkAction.java:123) " + - "[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.bulk.TransportShardBulkAction.shardOperationOnPrimary(TransportShardBulkAction.java:110) " + - "[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.bulk.TransportShardBulkAction.shardOperationOnPrimary(TransportShardBulkAction.java:72) " + - "[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$PrimaryShardReference.perform" + - "(TransportReplicationAction.java:1034) [elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$PrimaryShardReference.perform" + - "(TransportReplicationAction.java:1012) [elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.support.replication.ReplicationOperation.execute(ReplicationOperation.java:103) " + - "[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$AsyncPrimaryAction.onResponse" + - "(TransportReplicationAction.java:359) [elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$AsyncPrimaryAction.onResponse" + - "(TransportReplicationAction.java:299) [elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$1.onResponse" + - "(TransportReplicationAction.java:975) [elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$1.onResponse" + - "(TransportReplicationAction.java:972) [elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.index.shard.IndexShardOperationPermits.acquire(IndexShardOperationPermits.java:238) " + - "[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.index.shard.IndexShard.acquirePrimaryOperationPermit(IndexShard.java:2220) " + - "[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.support.replication.TransportReplicationAction.acquirePrimaryShardReference" + - "(TransportReplicationAction.java:984) [elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.support.replication.TransportReplicationAction.access$500(TransportReplicationAction.java:98) " + - "[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$AsyncPrimaryAction.doRun" + - "(TransportReplicationAction.java:320) [elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.common.util.concurrent.AbstractRunnable.run(AbstractRunnable.java:37) " + - "[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$PrimaryOperationTransportHandler" + - ".messageReceived(TransportReplicationAction.java:295) [elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$PrimaryOperationTransportHandler" + - ".messageReceived(TransportReplicationAction.java:282) [elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.transport.RequestHandlerRegistry.processMessageReceived(RequestHandlerRegistry.java:66) " + - "[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.transport.TransportService$7.doRun(TransportService.java:656) " + - "[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.common.util.concurrent.ThreadContext$ContextPreservingAbstractRunnable.doRun(ThreadContext.java:635) " + - "[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.common.util.concurrent.AbstractRunnable.run(AbstractRunnable.java:37) " + - "[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [?:1.8.0_144]\n" + - "\tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [?:1.8.0_144]\n" + - "\tat java.lang.Thread.run(Thread.java:748) [?:1.8.0_144]\n"; - Tuple> mostLikelyMatch = TextLogFileStructureFinder.mostLikelyTimestamp(sample.split("\n")); + Tuple> mostLikelyMatch = + TextLogFileStructureFinder.mostLikelyTimestamp(EXCEPTION_TRACE_SAMPLE.split("\n"), FileStructureOverrides.EMPTY_OVERRIDES); assertNotNull(mostLikelyMatch); // Even though many lines have a timestamp near the end (in the Lucene version information), @@ -243,4 +328,26 @@ public class TextLogFileStructureFinderTests extends FileStructureTestCase { assertEquals(new TimestampMatch(7, "", "ISO8601", "\\b\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}", "TIMESTAMP_ISO8601", ""), mostLikelyMatch.v1()); } + + public void testMostLikelyTimestampGivenExceptionTraceAndTimestampFormatOverride() { + + FileStructureOverrides overrides = FileStructureOverrides.builder().setTimestampFormat("YYYY-MM-dd HH:mm:ss").build(); + + Tuple> mostLikelyMatch = + TextLogFileStructureFinder.mostLikelyTimestamp(EXCEPTION_TRACE_SAMPLE.split("\n"), overrides); + assertNotNull(mostLikelyMatch); + + // The override should force the seemingly inferior choice of timestamp + assertEquals(new TimestampMatch(6, "", "YYYY-MM-dd HH:mm:ss", "\\b\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}", "TIMESTAMP_ISO8601", + ""), mostLikelyMatch.v1()); + } + + public void testMostLikelyTimestampGivenExceptionTraceAndImpossibleTimestampFormatOverride() { + + FileStructureOverrides overrides = FileStructureOverrides.builder().setTimestampFormat("MMM dd HH:mm:ss").build(); + + Tuple> mostLikelyMatch = + TextLogFileStructureFinder.mostLikelyTimestamp(EXCEPTION_TRACE_SAMPLE.split("\n"), overrides); + assertNull(mostLikelyMatch); + } } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/XmlFileStructureFinderTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/XmlFileStructureFinderTests.java index 4bf65ba7835..01c44147b04 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/XmlFileStructureFinderTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/XmlFileStructureFinderTests.java @@ -18,7 +18,8 @@ public class XmlFileStructureFinderTests extends FileStructureTestCase { String charset = randomFrom(POSSIBLE_CHARSETS); Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); - FileStructureFinder structureFinder = factory.createFromSample(explanation, XML_SAMPLE, charset, hasByteOrderMarker); + FileStructureFinder structureFinder = factory.createFromSample(explanation, XML_SAMPLE, charset, hasByteOrderMarker, + FileStructureOverrides.EMPTY_OVERRIDES); FileStructure structure = structureFinder.getStructure(); @@ -32,6 +33,7 @@ public class XmlFileStructureFinderTests extends FileStructureTestCase { assertNull(structure.getExcludeLinesPattern()); assertEquals("^\\s*

Wqv3z(@0eKZ$R|)_n#NKla0_^mO>NexQ7Mxnckoww~{`NDH;? zVphBdRHFF>=kr1Ph{5+|4_cxOpt$u`;gw9%Odyf9Ai#%!tImmODzA7g$yW`T_*7Y> zagMM#O3dJ@mt6}_Rs_;bd_23f)T;PC0|Ltm&fur}zoKw^>=}g#&?sqIwhA=JJ2Bhu zX%4M^*4H~N)KfGgiv0`hZO;nnP-%b1=(JYnY^a^oA;Mw0T!m*NY8Ae~j^Twf?L^A$ z)Ro%#>OGZ5R^4!-YMMB~mZI9_a8{%am>y#aFCREc&y8ZgP`@`-lO3##u+g3ho-Qcy zV~wfdY@6u5R0B8DXzrgC*pROOD8*CDXCTV*<803QJTHja%LpndpMy;)vAIRl3PcH) zTQ2_Nb`P%}&_8bb(<)I$gD9LxwUyRCQw5DEc!{-B)jV5B#AFgLZ{v1K9b}XEfk?%a zC?=;d;8YW)*94;7pTHUqK*I|=&Y7SPXQihuqkBEliEk*}&m!okr*sqC-8#_9K&hyq z!&%U@ZvL~R)NKcre{FY%GieuaKovDNaAs_#OX=~$*AF@Se~~K)s24nEJ|LfiLgDm< zJTxourVkbSXLV4&H1jZn(16k>sLTfGum9PwJ(6SZoGYy@#wcw_*Ovr2t2q1P&m=VP zzFImzt;OhxzRoxc5{sENIM+{UwM-RcNV{8%e^$RS^`(roHi3YYjlV0sHa9Ehzj_3> z*p4HG9B0{0JTs5J{g{>9our36BW=_$tE@gcDzn2n?v|5{1vNlm$+2Q(N0M z&@r*3)g3ZUq7AKBOOCCt+wniAX?Kz=`klf0sr4r=1aBMfagCBEKmmub5U~7tdeVOl z66OrT^9skp1u%burFNv$o-oRW_sFg(32@jks9+(XS`t^*05?prW0bbW(JML8$F>z3 zJl)x5-^aW0+&itLCxCb{fbAybH}61QrP`uw2`<+`D;+BS1}GQ3Ou$P3(_rg`j32Y= z@|h)w=USlP1>;y7MxmhKLpi~Aq=Gi_npXUl*7JIS-E%^T81jK|K9)83`cwvE#MNu6 zr}UO?Dtm=h5)#I^UG70F`^E3RIx-RXF%aA9|MLfWf^8@^Yc@Kz!L}6JwbGk@JDmMf z-uYoz>#0Q=B3$uol^_>1QktngL5k8(GrCO~C)K&TTcs^^5_X2njv211S>g0Engln` zPf3T4SoY4|$!6QE9>Rh7d2UsK)HhC=_O2x@ccI}Ci{)?iGLdiSx%&MV371o*{*mbt zV(ckh3T2`|CJHtdpGfhgX~@dp(6P${{3%^ojzh;{&&?cLRN*eWM;ftLgJ>R<^sFlE zE|>f`)`w7qKa~tnJKurz_Grhln7>r5$yLLfxVlN~<^)a?e6Rb8Z{2EHgDMp}|H-{R z5}$}Y5N3yNrCkyH%AQ5r(L;Mjr&S$t?eD4Tj9Zm}jQscx%G~bd8F(FC2$6!V%I=0% zS+s&g)~=8o63r&=Dv5@vuKc^xOFRRF1T^Y6J@+8r-h)OKN zyTXU@N|#^P%#c#KmAj@;#(6Brvu84_Jp3W{prl4ZzV7nsB@#TyV{-D`C~}-aQ(AFi zcU0((%$*NuuN^a_>$>X{@dpdti|c!eYs6!8DiT>!8^dA)UD~|e#HJz37l!E2@Fdou zKEGkZ>U8}OoP^5*7kc048x3zUw1D9*(_k95qcDLLw-V>^nY}N>_nOMgnYrg33zGpO z*Yb`jKqFF0;~*1{>Cs{?vW=dl6b-G>IV^Q7gRQ37&fl($k_*V)XD%~B?#79pB8S-w59mwUI>$)IC_Um>9L>(>EcCyui|7W5`0lgb z1;2TDvGURL)in25q?D5IvKx8g^RB24wd(hJQ^O3`o)K z(*+s7Nl94As z7>Tnkf-%=4e7!+Jrdd8G9+RQp$UE3utawbGQY=mG#1A&Sl3L$6!qug%(W+b3;Kx^O zLUlpZCU;DU?FDIQi}4S^H>#@AWeWRJKdz@k zilOEiCHuZ4&8DBpyRcN)oumH0RdMD)O`K5xr^6stuuee~Q9OY}maB}%6dF)MM8cII z5IMpnQ4}>(g>V>w5#$sD2@!z;g+P&Df)0>) z+C0do>eX2Xmp$)P|1w|44|ayV)^rRy&wx1&V;_=X`V6u6-|#12-kXm|4Uasw5Yt+% zS!Q$bhZlzvH!%`_e<$=8zItPO-I!at?C{KEm7uKW^(fZF%1el~~}p=Z>4$clKOL@-D8* z7arVB9jJ;s-JP*yA}@uPLRb$YS7`Sg+$(9|nR|0~G3Em59_TWB2x z_%RmF)l9tAvF4?BZ<*bVWWH!&ynq^N>_EeXL^9&89R30ELI@Bbz-2SZN8mqJL^;7_P1B}C%Md-OK(6Ga8r={3-y9Fdw z&;X8(N_oT)SIy#48`QbYg(fl_}TF7!2s+J5xv&{xi}>j@UN!vPRYbrp-OrruH9wHgWa(;->VYOxkGmr9gq{qNyG`k zBF+d7Ifb|&r(15SDR~M@T{~{njKm>RfiQ0Is$|2cr|wr4NLb8IF?~{473PJC!pz473Z3@ewx4yEx*3Ncs$4 zC8r7Y>h0dfWoKHvUkSKzzMds2H+~5kq|@Jp=xFJ5w(3#KJ5k}4NS=4 zaPY2dJ}4pH{q~EAm=;Z{;D}?uql9jcXM%;^*~cBdb*E@|HxLOgTpnt3N4QWIvv#Z+ zG~3#NIudS$6i?G)gL>^7_9-Y8v@v|lFM+b+UoltPZ>-M!&+AX6$kHdX{H}GP77O}| zoMS79WZcW9oY~$MeOf-rB>_YgiN484Mc37v2}|cNgi!*Ss6`@sn@V7gy=OG=pVC>p zPaNP=X1e*%kJcYD8vRkNjzX|EQc0i_NBi->D0UYtMBuFq`vm|fpU^5vog|KZxt_af zN5bh(=$$qX#kJ?Y$IJRhDl4GzteB@!F7dpD0K|9o?YLUa+|`^#EU+xoyxCkdSqgxg zg`1_x#{y6-f|W}RgGOSkTFO>EarxpYsL6u~2}(ui%UI^wILyS`$n-1n@6?{Sn)Nwd zfbC*JhzW_A+CZj(vP>+7Wq)qI5=kgjOvmr&eNIi6ng7#3y~_N literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/squirell-6-alias-props.png b/docs/reference/images/sql/client-apps/squirell-6-alias-props.png new file mode 100644 index 0000000000000000000000000000000000000000..a43e5b5be69275de2bdad06e30bc394ef978950c GIT binary patch literal 13045 zcmdUWXH-*Nw{ENy3rz$;x)c)-=?Vf8rAi5q(2G(;x&onvqP`%YG{H~>j8Z}`(nCNg zA|N0&6oHo#5CTCuAq4K`{d~Xi-7)St_Z#QjG0wO@vLQQr&o$TF&zkF*Ydt#(rmuO9 zk&6)o0-e*=x@!ai9UBBbne?ZDGZJ&Z9s+-kc^hd$K_xwx7lAJ)oz(QyK%nxtGy4xt z0pA%OYngk4K<8VIKF8WUu?`@R^h@o#YH)v>m5ei$4v2tvuDsfBb)(s0(%wCP+TBuP zBj&tTb^M902wUPedilsVQQOi>U&SKhF~W@2?}cKxlGQlNx!*+lvb@();vy``i}Z*E zi~i&?J^e8HPa!sBcFN}{$*-9ktKJ93hoe;nhzZnAKB|DcQ@RyETK<@oSFml0${H9+ zHQqk~0zK8s-C`lhf|E| zv1U0sBbX6TSm;?0sH~%P|4P`kR}_77(^fER^NW_)@DsZXfYl^sMX-JpIJbQ zvbsNcNjchHG1>_Fi5qgw$E&5?ue@;Qq%^6V5o$zq3PPl1K|0U~ZI+}<~#ac~=Thg-}(jn!40>OX!!f+unwDr!65W1-3 zA#wNo)Y&k+6xZXg`{nf|P4-(CkNP@zbB1p% z{)YCCE6>^M3e4KNk&FugI_-}9nogG!pSXRQ zm9BQYZ2I^X5LBrC^+#!nn)^b$tb9@FcPnNfEQR8y9Su1XU39Tv1d^BnlT!;XI33#o zH_fZiP13(*cb)SF;J3QqMskq_qM#hTRBw_m|G7lpUV3OKe_6Tf;wnpjIk#s63kY;y zKr9T5!&VrfEzuR@E_`Nb>IH1!U{O+7fi(Jb>z{Vsqf5bOp{D?I@0knSPIbBK1duf^ zMs8pI^OstVBE}+ruMCRTHNln-;?Ex zu!46@F#@Z4FuXz@;0wgz){0BLvKq)cKMr>{Rd)FNc7BMK)`_L@fk6Di;jpZqJ#og0*fTo(B*T57w($Kzd)?Ao_Y&{1@ILQO@c=K8 zn$T%yo|=`nh2Zv#9%nK%5dil?A@7)RJj!qLE&sw5gJr8|DAu6YKj_82CZErEjR(45 zyb%mvn@fQaL*dwZMFeJyng@nEe>$WNoTW$#@~FOoc{T;aQHSh#$=7buZukuw$9gGj z_98t4JG99!r)YA1F_e2YI!OW4(|4t6ZZ9(LQB=e&Ss`0ybMj-urN zW-o-b%~h-hHH$-OlZ3{fDxtHyJLP`Xe_liNb&zY~4sY(P_S_nx4P^yipFO>kRf7LC z5xBKlcTgC5(?aELIMFmjrdVu%5otjMfWa3d%uK2ax%UtZax%Ziz zFy-Gs-IrK>Qp4w8O5H_CKBit%Qn1iOpP6v3mwtn`>ozG!w$8ghev{lBDelxE)3dpK z2NTOMoLpFKJ*wFFtFo_7uBiDKDCrX9`&K7hENraX-|NLb3ci+li{pX^^-CBy?N2TUjn|05sI}7JLm#%m#Z1CpNIAFK1=VrH7MJ-?9 zO5iA0l#Ty+U{s62UYD321Yi z`@|oVUX^~@Hd~xeX}}j2^0Fw;^g;IqZwA8_=T5=Gt>KKl?{sy3Sv#c@Sq%#@WEnbD z*5Y>QYk7|m&L;7W76RX;r^EwhbUj0F8Tj9Nf!+*5D1bhhh+<#{9r2>UD}QKD2M^ZM zCC)LZ@w-Xi{Wk)4J?VXJW`CemPqcM^ITUC&x(snD0S-jv~Jm zRUO06BrD}doAWq1Hm^KHiTfMgnd8~JT@WzG5?&R4ssr>!lw*AX&9-Y9KcZLd#O-FUR4)O({J|0_(B(!i0W z+_F?@Ru>LY{`{(>!coyt$uH@a{kM^jFLyC})rARXyW+5(Y@qvv*R|i0UN1E>Y&A`8 z(vj0lm3yt%X4oc9H8pf+_yLe<2t9D{)88>9f6G(tR6b0JQ12Qt68hX%*LN|W!@lr(6s#eB~`9iJY=;$UIhE$C+=u=`4u)yJUHmcb)n(@ zgS(iAyS|my`2!x5g8OI8GLD0?9jC)w0;bY!h}UjxjFQzwvzV2cK%ncA7cGFS4{|?- z0qDRL(0_V-Hw4A+OriiHbMbf>Kn>$T|E(j}gW55_36H7b^AR#Rg!18iVD_nXyNNj; zQCKpyQrmN~uUHdX9w>J>J?<-H0(vU!L}D1aSF#W|Q9>#ex!{sLsZBWsx?eZq1+`xB zv#x1cI*pG#EL^k+$#4tLzaUt2uLj9j9*DsDhJz`c^@>6ilm6G&L7$q-bHdU*yXINB z&j$)QpQkHHjw({XmWVS=eN;{3?JTLDjD$gmS3EsoTDXPNZ4@=nO)j9ze-sPDg<7rN zho3xQI2?8Tn}RF8nl~#K(q|`EFhl0dKMwk22Uxb4@aTTMG75Vm#WJ=D!wxJB1@7B^ z>{c4~C^yn50)bw&C$SdX3NIY5dG66W4{3eeU+Y+-hfbR<|*6OyEu&qwa+1YF? z990?K1Go*%?JCA1l>xnC{`y_XH56Xj(>l;x(_yG!i`*DsmA(Gm$ZYCSSB;kB9Pmh9 zwIs0u^TKUoemKBPPX0Sbz&v>Z#{n>5wvASe5hJKD{d0c$m=sa@=KrIuN6|EeiQ}N} zjCS5|7iO^Ri=aq`=lt?Y%wVXsKQma3*_}R2Q556*$KjpTk;4Z!ikQN5nD25Q$MoF8 zPGpjhxRR!{GZ!$qCorrh`(E6}db7ER^s4nD0|QVd*g{%~)!XM*^^}0uk25E^i|Rut zV_G-G6moNu3z5t5*RLqMTGklUkFYM z&Oj>#^@b2gn?K%(d6LFEp%XR-pJ3HEh-#HSi}|xEwTJX*ehVIYWDiuLO+nxoRGv?LI-VaO^&CE!smT^^kKh^^&sS5DQc7M)4#zK!|3sby&-pW9vHIkD?8{}68!<&do^;s@! z0VC_(EH{UXm(MTzWc+wxeiNN?g`?EHZl>yGwP1x7#S(v3(-~dw2H(3+Dt2wp)*dy> zIH~5Q!#}9UT<~rODy&B?d>FjugTh;-7>8LjUh511FIvK}QlHII*^R)Zsma+TCu2au z&0YzFTr|s7zH0m4_C4ldZda$ITYw=8{=u6J>}mEUm_X;$V!{)FQ+AaL?mXr4LD8L4 z7Rd(vLczDOC(C!A!C01>KZ;<|HV$kazHan(xKZPTt1G?qwaTMQK|GYmQ)dOCToKvC zjP0aJ*rq!bomFE_zeLZDxps>kvn21Ly_MB{`8w04+f>~^UR-t1t4*WW5Bundgu4zR zhY*kAt9)|<)Qmor);Z2<<6Cdw^J>_WeFe9%^vLP*BjAn{{q;X^R;^OtUlI8d#=-j= zhk}FQbic6&-1eGD3Ur%0vfD8Ki2i~YZvPjL-#HZ~eAqyhZ%6z%hUuW;U*aW66Mj?Y zKoX}gxu-Btdw%#+Um0zzmxp;|OesTk0}PiDYxOi?@;+y`g4GJnqh{44Vr}DkY*4(t ztsUqQU82KB@hRIQcynn7I_t-Si_V2He)nK@v0!#7TN*_F1kxh4iI!Pph4Kh}vOMr{ z_L+|VjmP*Iv+%cDsvk&uh3aYfPH!YtOINN5(X+ps;2*5yAB17U`SG{AT@Y0p^u-?23a=RRsb}>v*8$17&y+8-vPpABmRl+o{ z(_h&#DkexE3*~%mfV=6j=Vnke?)-`u1cLj4!I58P+FnR)npk8tzu&L6W z&1`({MAtogsP*WyZ&6XLb;Nq_uZJPIEK9e-mrNBpwY&zx((XO+>GgT@aH(rO0y1H! z62|4VNcV7jzL@GYws{9l(Dx{kzVF$*u0cxwTOOagfnl+9AhV=58 zrJky9w4%~B zV!)+Qa&}=0_e6{uGGE%_`|uQ|#}NDdvMUAp^6tmg!_xBg~VyG?%^C_P+$(zhrkU6q2bsLAx9zPakIvK zq9jhH=SGV)YNg74;fh|5HgEbQ7WT21)VQpP35u0lx!=9hmfOYD$202Jm7K_z&b_}Z z1P`mfFc>VLo@w$HY&=S?qAZo!j@0`cS~x=9*jn7=4aR%`CdRuxAJ7ih1x9?oQ6BL< z4|E@QR8q4LXSyAo0w+0uWhDmJk!ys|gB8E##8BJqm9M6S2N5N61ip7|U_U(nQ5&s| z@n8eB8ehWRUC)gBvdV}3vDG#$`xwp@iVn|f`Q~+$>pCS+5XdRtWlahakBq`^ZbN#k z1IDsf(Y-YWrrB*KtBpZ9T=UjvjGNv-JIR>O4(Ho~zUg#hbbi)JatM-q9vkvW&#YD6ZX`PM6 zdp^G~)~UK5hYAkFtR??yC$TYJ(D0tP0nfpiboNda(HBdq>90`=fgZwCBb~jYc~~KC z%k`l1j<%YTTs5JaO6Ie&o$3qOnYN@L+#R2=SVHl4tPogEI2LT{5vu=;T2(d z=}Mq42qQR{bHAUXzr@ICV(HY=N?C!y>Z6QmbbaN_RcY%k6G>3^?`$I8#ld>tTn+22 zhW*a@_vnT{I{SBu0Lz|+o_eG}FDF6I&UFHFyfWPxYJJge))ioGF)Y;kCvM{+gZ&-^ z|2qufKfdPQ=?JYx%21-HW9)uO2xtR+-)}eB}c>!)WIsZ<_?#u-@7Y3A+ z6~4x@EV=Q+fk}~>zRDfSbKc7HCCqy<)}+ERBQ@-A7NEJd(<2yQEVz8fH>lIt_Ge~N zfvr0iM9mFz8|%)t^lm}1O$tb8FJYuB{em4o>YQ-njZT9nZmJMxUJYzPw#@Q|y+5sp zdzvqQi+vgIHShc7(Kk7Y>i~nD)NxF;8kTq^;Q`S?2#&3I1Q|S`RHHO&92!K-W8DUB z6&?1@8~=!(^(~+QQ!-C?I{w-KUVP&T-^k5QNmUWWHZ^NSHF046y;uy}Ksz-~xNh(d zc9>xvt}Z@;aL2?`_qEr%ZXL=AHnrgMpkxADBz{uEsJV+MbAA-mX;GQmU%fk)r zK!#WA*{vpLNMv{)iywDR3s{POTGpm(s9BFbJCuKeKk}*V_p)&p4k%OS}YA zTpj-0i0)@v7Pp~ur^K_rb-x=|Rq}4?1KL9G&@Bu}+15+7+cF#flKsSi*!f^LXgh60 zYJR3D+8j!q+emjcyF6u)sG=wmxlISnNQ8Ma#D**y)Eg{0rZaf&bbt0OW0!zRL@~;V zJgfIf{ob-^w%f2xV}_rmkerW~ji<2^*8-^=+Z}`D^R6(3*z2L3jSY(}Bgptb*N`_C z+Ue0|M`V!|EZc4st|%3!#V>Lp=JD3USi*p6XwM+WVEM2s44ryOGbPk0nuxAnPB~%d zlI7CxXz6^i7N}?0s1`iU z=^Ac9(C}>xx%_MyoG|#6YNpa{nHU$55b1a>4B!!&DKOgQYddFYI*{}^Kk{YmT&<0; zW~qh!AYL5-Bb!q>&PXVWcG#g+Zh|S*$XV#ZD3)h2OAG5pi0#{#uzWkF(S4)hyKhjM zq-#kOzUT9;5*Fe7yh~zV78I+C!y-tGfV&@dVlUON+!f-2YM>coc_V+ z4;WCtu|EAT9x+mOhZITB({6rw&79K!tN9v{!cTAa@dS{%{>iEU$kmMd6}CJGxMj5x zm*alY);4dpo;KuS(JVj4_}i&u?SEDSgeVwLLCkZ|SDiYlK%M-LswG_O)sfjej`Ed|19R2Fd)!bDqfikg%fiw$!4f zcmkG|*n@r^8`)RGldTzUQIzRglQOyvFj=t^m>z9x-P3Qw`JIJUsq;ToQBlOKO1ny^ z_`pjPCP8Ee(Ip@Fo6eu#lhV;V!T=XN&|LUDz{USv{`)^dmCFzJIY6Ht{sWH~skuA4 z%qPo7kC4g<%n>ACst58$bfzcwZ!81Z+Yq+!z-{cmI{s9|lQ=grn2gvob;n%aDufgVj?Wpr^)q3Yl8;>8Ngn1{_h zORCGgenDw()>e|X;`+Tmr_Vxb|8i{mHg1#{v;JO(UpDCTGz5 z@+0%FZ>^eHhQDpuWN3H)d*VwkgCe{>$C!lL+r83~)!l#$xSg#Kv>qy+bju-`7DyQi z0&`)c#_=PZ>eYLk>caQg0F&pe@F)`J$!oyIa*qeUjI6m)GuG>nR%}A6yAP?UAiADf z+K6c_p~Y{gNLn@m=r9I={vHojF3K?#orT-C9Wgf z#RejOE)PG*O9fLq1t|BH((QG*1fyki~pzj*YE>Cr5T%)?vHX{my z2`k$ZOOH8~r!+1S$Bu)F=)$<-UHbXXn_4%GKgqb1Q)8m$;fOg;r%d@C`_p}L<=IvP zQ)1_?_d~adgQY>~M6=djhEMOG!%igV?S%-E>TGI&@@qi!W0ii;s~d*7wbg|*kL_nG zbum*^g~1b6zXX)2yj9GuCzv7>9AuRU=V-%z1dsw2`PJCwD-Q{$G2x!NBO$Bho$72 z-quj`nus?}abGN0ovnSmUzg@;qHs;}wOM_W%I7%NtjsR$4Mcb7mZs-1$AL91uCg_4)ut)L zsPDka-PGD>PQ3q$LtbKD)^`o$&+VMy1=3*0>w%nPDjma1)uR@bG&hGU<6m|bocXg9h0 z*QE{hJ}fID`1Ub``hY&|-Nqj4l3g4}=yIFz&{t&uD;cuO5rE=wUD z=v~ZzTqpdyYmxsq3yA<^Y9MHdDehISCz2WbDF~=8#ikjNANCh4Z56)M zkPqG_FHWgmzguw3Yl7wX@A;-!!S8$rKMxYJ3SD8n*lBh zU0WlVI_$N30*n8@@J-+9O8HR|i{a$Z{{+!)A65;lz8}qhSA3+=0@epSW$}uFBh>aH`vINia`W zI%^o?bv~f?of@XtJlkECoqYweUz69`Ct8=a0&Z%b7FRDb>Gev6`YW&RfC-Uy+(v(8e(g^n;sE_+F{0wHx2jGc=D?9$lqiRv?b&;5Xet~^ zu-*&rV}z3?c2=5vlG7>-s5J}8v?z3Ppeib$=35gBMF{?s@)U+KOelg$f=#f=`{mT) zdxxzVPevukZswTA6*e+)lyM7f8a$S{#OKFPYWaqQlhkkivKwBpcNZ-pWI}|&6p{+xanM}Tt55@Dow)h)7+}4=$lTRu*mTE%he)vfr z{G@SEo2CvPge-U1+-t;rcf z+yYxH4c4GE50M^}^feR}^`p_%H|7bAJ+HPjghZD~Dy8n&Z?&G}xR+H+blqTQk|SDH z4;5R7s`O_XKw7a2qG(mp`uGN6s&2wMaQzW3pcokMf5l$D9*WvN(2kZUi32_h&~Wyj&+H zdG_cC5UK2vF99(tkzcA@bRO%vxn(nh-nHn-}D!gJs&@h8z{`a6Pa)&rSv?D%a z2Y+ff8uvh}&0pe4v9M$FC!paPqdyth<$;abzcPie6=vP)(E*#fAT7}UvtxLj%-@T^Ek7yhc~NT;RJjvor-60il`BNiVtvAv>8si4 zPg{!03L}4B8QyeA*}(~UI(+^@{c)x1TExh%yyARriMb{9R9U230LJ1$$u@3u?mH5% z7SbS=s6NvB7gHwhR2VetMVIEgR`&%a)9@$F&RE)D|CoHFuK3f@tJ`64rF74pC9(11 z=tCO#Yu8VQ{#9aOGp%MtzWt`4Q@3C3e1qq`^$X6Ka=J#-*$+*J%R}yE>I53f>kb@r z)-{9%WVB2dko;Z|&>`uSMQ?4A*Mcs|xxcW_B-Y0fRJokot;@IFY3g{R9-H2J-=!~k z?ygy)!KcLo(m&igpcIOsG`O7=Nt>I7_Lj%;?UBCAStRE$IV zk>5zKz58qajJTT}{0~>Nn0;o}5_3x$H%n7*R771RS;p#mgo`1i8ALghOX!i!l={F6 zxZ~x8&&;O4$Wse&kZ;@j{xhah_0M;XC)6)jG47?-$XVnQVl?CIY4jZ=@x5zWb!IgA zxI7D|iCw#`dw2veV`p}gQdMly<-1VIRYBQI(jr;yxte2V%H3noJJBV{AqQVnP;5(d zBRc$W8X{Bg-5w;QYN|`*yEtCDFZvLiC4ch;WoeynGL&3A?*+Maj#TMgB_-;zq56?zr8rm{mG z@n*?m4rjLrp011`mtRY@FV(b553hT}2J|O(D|xf>IPXn3y-OhMc=jiGW;4my6f)bE z>oJjn7~ z*cfy_4DqAs1n6lS2vdC$S5Lnxd>nL}`rq53`p-+G|H)s0P3H-mpCrH%Mdzbp%5s2> zx}&uWDFe4K3E`uS?A-tAuG~L*^_|h9ela&kME;?0QfO*qjCmB%R>aQ!1rgYngT!|% zka4RvI=Om9(aJWyeW~)kW-83!%H!FrlDMDOw|8qF+yoZc8RK;tmfyFG%+9_bo&;{Y z4PL7t5limiesp$T^ZQwXd%v*I_AnYRq06l#&PUH~p=QiR!dOGrk4c)3BERNEG}pOK zoevFZZ`?fYVgVl8N#LPYN5fX@N%7-Vc3(Wc?*y6{*?x4kk071^%#ZwR*X{X8T3CNBmc3?-xtc#$fi2O8EHsF1?;+|u98s@-CY4RgmHRACJ&UGKx*SX?4XNnqw>Ux%KK6bXS(PJ zUPAwTo}E!c31>8Gs2Yi`6LaHX@bn~|8()59blzoeZn5itlhrjrtqd zCU%tMsF9Qh0a5fw%P;ma@_N|zhex%QnHVF+qt`#pU;Iw^UP^QwJp!z8B>Mbbq=sec zz8?-KNSHsIgTTpv4(L;}F?|(c2Nuu?XhSZ>M#8v5J9pGBsb;LnTvAOJfTC#*&h$su zL5=9QE?qL{xFL1N-rpA^g=eEKwL7-*YQ3Z-rba>0FiZg)fvqSy7hb?5 z(ve`6)IQD(C}YG~!(l`~ZHEixLh);d7f5G_!OKh73p}VgIe~mt`!(3V1+!j<@M;wC zApd|mpgoQ^!TKTBw#>Ih$FaB|Fwx6Lc6@vO+;gdvr6mZr&slP&1LLr!_4Gy?veo%O!_uCD*S#uyy zofF7pnTqslRhl=`1HC(sVWc6Ge!0%lL7DY<*SA18qsd+LCazD1wr+t9dP4Zt( zJMvk$NyhQNqU5x!pG5oE9IO?cy$kC*l6F7y*T@h$U}1`iYt@?(dxDryMfVLkPx!8; zg}LMaO4V`%psn`N+J2{A)UQn`)z^w}47D&Rk)}7tGfIn%;((q#I=&YDPeruDny6AC zc;HvZWc`qH?Sv8n`?1IaRRUVxp%fSr&6QZ-1635tNa%?&%lRN^N;$=! zk*tQjPb54$dmfNMF|^9eaIeMcz=N*X^dlCF0EzWt_KMBt2t61^KcPVUnY~d};Sj87 z-SsxH;5k6fA00==_-pR7y@d1KX7y}y;PTjdAzjQ~5XN3$o%1CdJRGZy@R-wCXx+w` z5gEXjq8R}tIX}Cgp85cwCeMOp&s+@#T@^82v(n84!vXoXuCT0O4xs;LK-7dF3ijqo zbr7b7U=wqrLMucW&{MtlJuHWj5k*>U#wNLYcS}2cCJK!iP|$AN#r&6}qEU$7ez78B z6|wr+)15vSQ%lSNDRaJ<2!h>JY$(MHQDX9!B|s5C7cO|tu|P1U10H@PpuY)d?%7>3 z;G)GQD=&Xr(u&a;cuzLu$iG{x)q-%&&@F})OMZADLqCx1Vp`ar3M;}`vGZ^q$-#gZ z2BKaFR!Jjjy1~W*Tn|JftVD^)CfYYbBWs~J9Mw;fY3v}h}dXEx>AWe#3q4$nL0O<-! zFNWSBbVwloMW5$+-~V^M^PRICvb*=*otd4PJM){_s0TXgl;k(a&z(C*si~o&f9~7` z)VXu#J4r7CS4LOMaKOLwp8D#_=ZgARmw^)!dnIk9bLUEt6a?!_z&V+lhKc97b5yNo zf9E?~KiHl-S1hfmqV&+$a{Ywjr=6i6-__x3oypVI)avaA2btpcWHXn|p~3CE5KRZZ z<)L^`x=X!HteSyZYIRP717l?)?H%q&{dcdGV}0*k%FfmhB-iJ*yla!K@h12(&-^6@ zJ-v4{m*(qE<%g&G75d@(<@@XAQ$c+xp2(w&MwkA&;ye9gi)lXN8S()aG!uBw4k~K~ zlzoOD=%3Sl?zTUd6L_W345+_9=N{`r{|-(F(FFfHZ{accDCG00w0YA(Zpr3^arphI zQsKZu9**a;um8^b+^q!Lj$aj{;PC4ZZcXV>6b)P|6eqVj{dOH|(cv6Q-MDFJaZEkD-TKiVd6-TW^q|_NSw+Vb!eoimkNtW@d$;KH*mWX?R&`-}bi% zSBmi}K?TA$(==4Z^X6x1&eYWEZ>5}jc9t`X;(invEHKy`PB$F|YkOO7!gXan#>y7& zYn6UfxVJLa9`cURF0q_9t*IL#<%A`r6Rs= zL}w?nQ8PPnlg7GS#K#`&H!5AEpy`^e%359;EpVe1G-k2XQ`K}rVq`rWoey1$ z)E;r9mo=bm`J~CIik*+D=V@(K9~chun``w7db|4i0LKeABR<2{#ya0i;H`C954INbE_IjY}_i&^bKgQ z4x`WsvXmlHij&u8oJP1De?w(3Y*eqqv6}qS)|Y0(Q)oE7cP53rnY<*5To(f++AFk` z#dy}7Kcb;gg}L9%74q;R%=kBT9k_%KKBJ&+;R--4lH!6Gn?W3CoPRFF~r?dY{HHui`Y26ZtLZ+ym%U}tks-F zdK}WCfUS{K?saN6Eagr0ap9Rp%Qwpvrd?eqSQxbUx%Uy-@$7JIyVs#D*DGY%`&AW+ z{`)gu5yuoUo9qe8^a87Z>rkR9)(|;u*&O?(?DlXeMWdW&wDbHhFX+Dasu`ej9{#Y& z1Hzqp%&5>aoRl~hSygnWtGS=wAz`XIyt>hshjr(}y%q}MAskyv-G3NqMX63WZKg&Z zg=JFdp12-RbNKF1w8&cRO}Nc?5;2uLiM`{Kk7b?mP`%evKaGyaMt0&SH?eoX`g_7% zA9R2AXr?gAYNuHQ1USfynH-mBfs3bFwpKQ%~qIPDO6UMm^FYX)&5l@wXOMP z``60ioV>#f|`6m6p0m_ptlQ6u;c8wPJEqr`1(mye?6JFhx9I$KL7pck-QfB!U77**^8&La{6g zkmZ$ysk);lX-+j4+j9H*_dM5_aJCmWtCofPa+~D0*B>(%R3W{pD?q=k68PP96-YCiGArcmP#u&(T0&07VE3cPa5l5jEB z(<{wP89SJWI`4$oYk$esHKg1ASDSIjrVy?)AZj-e%$%i|(UDgFX9`+@CyW z?r$zTvJta0yhdmLSwNYt_;@NtmXf7yj&ZT^PTJ|3B02x*Jc6`C@$~yv!8HEJ4YNs9 zmYPunZsEiZlqI5MQ$YNETenh_kw2GoZ2}Z|i zKFxmQ%^>${e_5z1lr7Xq9V{JX??fdSax>{t_AM}bLe*Z)%7YF_-nRS64j~XH=)@v% zMqWWp4{U%QxkHy22iu-?m62({3K9ib50>y@_6j1i2*UhUk2)dHm*9G;@7uL`s_eTR zvdO}}=SUQ555ijQ%e@gWQ_yzr>My41 zF@^TYF)Fq(s*@k8P%SI=s9mkcl!77jVqPl`o0r7xu0a%pa?t=m=B8qw(N1)6J2Ynm zm2V#5xAOAkMbfsIBSIHXuqQ3$?6}WZ6XLvN3#y7(zja95+HB!CnQ8VKjjb$Sq1_=jc z^ziE%3(^DXDb3s-^zmY2g_&mJM@sWUy2ap!#Qwk!QtJ>UKHn1|OJYTzdx=NCIRjnsSn1B};x z4UFNkS`v;P{Ml@0VL9lZfwT8Bm@*CyiPugEKJZ>jE0VqTW~$WXx?jnF*m*nWZS4Qu zj}wyq-`h3l|Glr9L~90flAaw)6#xE&-4p<5eZv?j0K!xmZWoyq(5r-BLSV<4tC^-HvJ1L+#~euk#_Be~+u8LtL$B;uU8O zzcbhgyB{s9hw|t&H@J~K=*aHL5p8C>SXXTS{oqY$#ksY~hhC#S&u(56siV5S@8Uv{ z`ldxT=w%)E!0+u|M;B_^ITkN(zXXYoq(JSPKEQ9cdc zz!*~%Z=+I)pkAAzty&*p^H@x9{;u~>@O(K{&+$MP>7@EvVgZv~$ z6Q5cy(X9WNowVKuIxhKtu0js}c%q#)6KZ?h5t;G**t_QUA&)X#DkUNlevHFLLLXZi z)hHf{vgLo7&yV1VZ|1z8&P6^zz)26x|0pP&2$!?W+_=e57446q@5*MSt&9J4JX2aT zrzjn#o!DJCSg`WAU^F|NemD}Yq5AnB8(Br#7qE6hL*_4tN?8SkBEQ-^Q zBG2O=`P>9$KY{fnTC^7?=bObpxA3$c%z|IMN$Z|6nXS9;!sKVzWg?zw75;2=^(65_ za-VSh^lLJ+l!N)4eJ#K6Y=Qdw7CN;N21A_dRwJ+7&6jxq4bG;1g1BWOO)DVfdl83u z!(;<7uXiujVM{mAv=*z0E#$V`d%b&eM19;%S0zKVT<9L?a`&z1X5*rV=}(LzV6&nh z+jL>9Drk2-zlJoeVHi(0;xLsyq{WgoLBjrOM1IcdR>UaJgG+tS6>o%lX9H8+0CD~TpHy*!}E$_=uoQl8@*lnNtE!^`d>V5cNYgAgy+GZ z6(sxjSVn&nap`66fes?7)`Ay27QQ{lC#id&wC#K!E)R1K*oY0X0J@U>&>S@5`$k{(wEP% zJ%QM!C8fA?S=YYPh~r34&XgRWzF`v}@!Am7-JLq#**$DK*zGxEzxr-0i`%8{SF!B2 z40Md{+YZO`VDnt<1A|YNs%Gep@zm7Q4)cbpwaaGSN{v)rcuMz#3yFXydfLhgSZK}7 zadqguS24Mr4jGIOjn0|+h>bnVWQm=Dj>cg@&hxaM$3KfH4-4}6XE--jj#lsuYhc6) zO8KLQpVwPGwgH=SH?8&VYBjrpT1}f@7%aS>7~B6H$FYx5Q?mXQyQTTh7&SqVYu$2QLiIE}Aa|eVpAp@; zXu|PuGrL&L@8D>n%#Qq~AGI{Me~?NMec= zjLZ$u2VwVUYcu91cr`P*LE@)f)K7GxHT5L~L7hUi)^QQ0&?dsXICb@e$Nl%k-}bl{ zL7K<8XeZDCa`XoyiqK=5K2g%%>KEA=BUbkzmsux1IW3iU83ij#VL+AJ=tE(g+u(!H zvoR(yor5~EqH?<>5#bREyCoVmjlSA)3uv1b>j<`?sL0h(&WP8ZDBd7_D8w@fw%syJ zj9zVK#IU2XnGsCS+jzhALybBq9p%#AqcNJip|&-%c?oU@@>!u8Kl4-36=Q`_H5rs1 z*Q6yi2kbe(N2QWP(_H3-(ik+xKSFXbY3pFd$F56kq+`DT!y7s`>BGhv_YQM9dwyM_ z_=NwA-+HG<##Vt3rFOsPw6qypt%r+KbXX#}V008c_o!RRY~Kz>cPi{Uy=tu?1$WkJ zi}BP)cOn~b#yQtoRxcGdX0mjYX+ocsE{0Ll!j((c-#e4@3hbs*Go0574(NAgRru%7c&X>v4mlQzLR1@a7H0W0jIhtwj6fwo*y)C zJCI|aBW?j6-IG9FIdBSf9G52R?kz7z(p~`ABX@Lh=qBvL*-JV_7ezrcA0rnX8)>gO>AkLyT0)vlz3r z7qU;6Ut4`#k<)&#Z-Jlm4U+m?W|D`Q`(>O->%Gs~kv2Xj1jw`DcOpJ)lf&0!PMhdq z8z@iw)jZe!%ZUypNb4|qC*ymxYN}<+^E6P^ItLNoP#j^OMvU8ka8Sv_hMI4T_`q#=KCY+;l3s{t4ZDWYWM<9u=CdynNQ>c1eH}X2=TgaVB6n@ z;UaR2znH^$ERVjH9F-kJ#3e)b8^l8IlEf<zKg%IQKJ+ z#+9f$Uk!kK+E6WX$a&iaGNMQI1)91LTTfr3laY`TkE$*>?_ZtUmR?@ zkL+$QyD<(8l@>bqcZpMk(C!Z)U27Iv1Y7kZ`cLt4BNJG z^|!5=x$<@1*risMwPChs>qK^g)HMi8q;E0ozzn-u>>|v~oYUUCB2ne^=SPiEzfj!g z&9X110DM`Tf6hQ+uMXs8O_tLdS(#sA>P7a~$*CE$h}qFMl-iKDL6Qrp`50y`-|lLo-9NBjjtX2wuqx>CV3LY*fvz{!+_lzhGzrkX)cD&@1 zgX`{yq5H&XjatN>C(+`t<@6x*{*=e2hNd_xHz>^XVN2nxsxzMUJ(1a8 z>yDl_+vI+zKMT!M$^1Ziv=c8Q!bwE?)L#i%FitxbXkZPoeEwNmyzMP|m&t2hJjUDZ zUUcfk2^3|Ec0^6z!X0V5n=G+?>S6VEH@&^4>=fGjVaZV(iC4R z{otoex9jw5KPYx;&$jHAWzGxXP&-&`etR{k24=K8%V`Nro5D`~ad}3d)P69-BC@qh z5P{1poH0)9P2VX&J)&?55(U-^D)U*=V@^K(czaB7irIlj0s$x|EufsH;G?YJNpZNz z+`VEvqnFjm0Hw+tvI)2AHoR^eW>y^Z?C@0a^ojNZ9UhPpZDP$ZF7@f`vw$(HmV!*X zR5am>ATk=ZU7zAh*jVQZ6c0GD#Q6v9ABV`gQYhA~CbyPZS^B>K6Zi8T`XM&SD!-P_sRtFu-xZ!(~ zLsu32iEh6%71?WyJ4*r}*PhT>%Ii3xP<({X6uV3P z>Vn(ig2QD1ATftvfHMT^iMw@uAEw?=>4I1p6nbITK@QE}AS@xFQTAD>w zA9_Q!rCmKJPtFqUUA(=mOZl<2LG0*dJ z%Vc1*-zeevIl`8u+U3Q&xJN6bg70B+6;c>VBQ8;9)5i4O>qRC})1ne0A`RWmZfies z4eXwsYDfBhI}}?$_VCAU#yMGbEsCul9$a%~@XUTL+1n`+m{XCdn60>xHfyffuQPt* zyibGGPa7BhxI<4Dl+EMJ%7JLjtp<7o82&@ecbpm0D$P&8-y&q{9EJJb@>@hsv${0y z3*v0);A8DH;E$@jq}6fLD$cEWC^@(3UoNTWz{QOYHSp1ita6QD4*Zg9CuWY#XPRN7 zX=jC|>9osjoNYM5CzBvGY{Y^mZwJLM*p>-fZT-dp;Ynrzmynux9@T5Kfq5K=O#twhTWu z^?Td(7{V&v7^q=2BjVBewPgg5oJE_hAScDMY7q_JuvZw}3vHUeW_>_8h04&RDv9bj zuA|3vvxXxApkIgLC)wwAISj^2#jU=4?BU-5b&tLBGYn@AuDWXM*>Yca<^f@bRalqz zP39-FUphNCz){l{(3OsncZ{}zfMe@!DRKnjDW%8`dGTgfhJ=9ldRM+R`r^|!%Frf0 zdK7}KjhFB|EfI~mksp>u%EXp`AFW%wG%18S zTC#04t-f_zweV@XolyuWn(I7sU_~sAlra-Gyctmzk(bmlJP@<>{Tat--&kcj?lR5^ zkK-t^Sh7!Kiv8@;v1j3Oxh6E&o=xItAa0?<@@RF%cH2P@+M?T`8KwCkrjwzx-8;%& zaJk`5onVj6_$e-YHj}1p4!O~Vj#A(17?t-OpY#c@Z;9hF$A`Q592$W{Tw_i(W=Egp z0Dj?~qJ$vFFkLj7bda?^ZC_786AIn1?$nY={VK4z z$x$ZvgGp?WEOofs>C0tuiMM-gql9|#|nr!~pQN=+6?JawZfflVsMv84U1Ll77X71EC761!itU z`tym+^{2YR2D<~^S;C;BluntmNjo&Cx$T6rqy_Ng;~HSiX8;y=nVZ8?_ZyI?008Qp zvuuRF%~qlo(Gy>UCIKc2fdxBp-&c7&w9cY9-|0(pl~|JC0Toc}lEZ|#?FSF&Q9I#Gs3|?czvHCgxj}{jM0nyMqBOy&xd;e4I;tcHO@vGKAS z0{RsI=6d!XR~bao(ITqmYU>-(De2JYb8qv}iG#YTL3K4gRwPR6$ZzKlEms2lW2;|JAQ z)kZU+z@VgpAN4lqrVQA`Lp@RkN^s17ex+rD4duJ6KHS%tTV*2b^Tfx~r{<bDyNX1<90@5=Y*Atb0L|`xqKx4U=x{%BFynCUO)PQgx@yzne@Wl-rgT~ zMG}5rQGAs9^vYjUdPwk*7MM&0r&mJF1OoBNmC-!9LFdAV!ZH7N>TE)cs&QmO31%gg z8TU6ozJFBqLSIG8Nb~j66}dPxhS^Q5ZYlfl14}lpYTQNmQZ5rp>gyW$f6UPFqaGW_ zZrBb8f#$`PwL>k$$`WtBscCuYh(^b{t&QG#dOJy1aUnj7PQeO%)VLWW-atIrL~_&3 z=uv2J5&phE4;k_#9jMxm*Lsg9ogaa~g4sgMjtk(jaMJ3hjw3c3(VEV>GmZh5Zgek3 zc0Qb%x7o-R&7(zqSyUt*34n0;1a5&lSlz#hKB!lc$d4L)><<=%6?RRvdn} z2`B>`|7Z7CnNVly+8V{5acy%mXfq%p^Drutw1YjsNi^dXEa8#Se`fX-MmIxSe3;#u z6~TU^sAeYG4H+7M4YFawS+ ziSb`{AD5uzP7h~udIY+vO;vUyseY#aXoO=^qC2n1{XP$-!ps>OTQKU#Eaz?k>OB%i0g2KSWcXuMywa9^523-- ze&6oRIPgYJzulc7{Zq~b?k1;7Fr$KMs9tAg<0 zJ%3^5x~)zdU2 z67OdXZxgnpT@DjXaP@6vY3bz8WSt@`NJbzIBM_vn0psJ`y0YsPTPw!RG7W%`V>gO} z)*Z3GzlIdWfm)i>#l@uIGu@?`M?Lll6VMgyjR? zN>2IO@B*|@Cz`jFbrHwQ84HCzjM8wrtHEhrZXTs51PK~=R4iX>V! z`>UX-4xCK1WKKSf4o?};oUHta8GplZvZOvM^>xZIN|Nm?btX(MvAEQH)fQiqkhD6e z8&>tV9(=IYH=r=a?EDI5h7VZ3fDPZ5-}`y6pItfYo>9fcpmN64{SKB z?=l4VLwQN~j%n<|@OO&+v_~2PeC&Q~YUx5@gkE4b{PvaIvavMs=Fdxe$Mwzl`la^I z_T9Ja%*I3NKQ}U1;m3xNK;97ueT3q6-p)0rm> zt=D|uZBfs;ox4^}7x=D9_CB5_7A+yjJ#n9ZcTAOJdU6^++P5ZjxZq%hnH#mwocsn@ zMAL*2*w2kvR1$e8#O+MleIQl$`w9_1_>^qlDcOYmz>ZQb_{Hj+O3d6SL z38f~0R3b?8tLU{HIkHQmA|>vF9VF?wNJ^Pd7OJ{kps@=qWGXCUq%N9+2Z{P!gZbv# zm>lOsiVI|}=lbM!dPV>w(Or#qC6v-_xX1!apLrPSm=pE%e#LoAP6hQ^^gETB7{iSnLd=*F9F!<}8^08WtWYkEDstPxFWt7EsmDs-ardswk}pqdAEKlj@Cnv3&=y z%O%}i(IH_{t%B<5lMNpWS&N|}KQaY3%c>u)W$pgZMt7RLF+0m(>a_gzu$WHfhToTP zo~V(Vb6#T|696dt5R&c863}%MX%RYO6xI1zTtSHS?Jw;29I>KfHvfk;hby1&E0(1I ziRfh#6LO0-UZ!z#xA_a26q?;2rO^sbN9q{y%O;7|R zp)|5I*rR0w4|okXG7)0<;ys52NB@(zG;qnhMd46Emt z+UBxkTl?-&Q>t5}#i&Pjp0WyoonInfoKYvu)lYWo+>s#&_Ux#}A z_V-VQ{SnUzAT(u6Q5R69+kvG5`zazfwFIRAg&9Y!hz}I0~=s zfPiZq@%tr?&Ekh1S%Q=a9kKxIpY&uvW87!`U{(-(eY1Nj>OtYLeE~){xw}jZB!Ts9 z?<;X$yl&VGg|_2w^`~R6;<{UqW72}%eG-W@qO-arD`m8)sf*P9*7pFVJ&9oVHsvX< z6H+V$UYsD8AcInrage`YFwg}>Z)5sk=9i6@MsP;ZQBN)a#od=u>g-OXPc3kQ)O2K9 zJ6FD$<{7(DOq0NsHY6aBx_0m9<#5zQIdsM9TQ7F6&Jh0j4MRjW0*MaNxF+3KE84wAa0q@h$x{n zTawIu$%2t5B|5b6M%LvJOwB2<)78!ojCU2g$Z;+m;h+{;YFs_}ed(0MCQM_)M_Pp6 zC498zPv+TTiimg?8V5a{UWYX@uvkMA!HR={?@qqtP{fYwskUfF zi8jwpzKk4f|^6iGFjV`++xo#-OgaK|CV$Thdc_%ln-E&8#@CEn1dK>Z|bsA{hIDyCmu9)OR#FB3mj-aO>G>Ry4c z0+VH3D)n8kcX`IE(@j&Y4HT{J;6CeU)wJni_;0E}gN-y=MB%=B1ElhJQz%UD%Y~Q6 zk8!W+tV4bsd{mV&giCul+0=Y8c>t-@)q)0>{l=19;TN^^M+&CWpq^f=s3nC|aEx`7 z&(f~1Ie%d1QkG+3Fa7GkMfQv<>exnG0qD4#apcG%&z>6jzy*0sail42Z50|h<)+l1 z#1~3)+?l~{%0&ia-DgCV#NB59k?fmp1PvBYoT_+-40AhJR_8iaZ)a=g8M{6YR{$?{h&{*ISAz)F@&;vKNsV!e8nIcnqvp#{-Rg87~1;DRo9vH7RZpG zQr-#}ofG|Q<^Con)OK{(JqnGQHtOP!{W^JFR^)B*4J#E;b9N)QXV7f zDYbMEy`JDfMjS&a&3GJRa1MohfbuS_&WFd;sfsHUN}S5)Q#lmwk(IuXx(De0{?l=% zFbk6RqzbVPKQFr6KMaW+R3~tWj@@+%NPC0EZ2Dzq7NA3$nVX}QI2>FPse|5fxA3`e zfTP@Bl*W5xBe))NQGK_k`Q(ZE02ra(*b!MgCOb93mo{7j+xbXHrZQjS(gD|;1w*>hU%G&$1q22)6HldMr zZXw&1F0ajUeJW~L2-L4oz>q(rFcE~NL%GfFx-aT!o#{sxV)WxBJ~UmzV`K1jRoIM* zJ_#>peqf6WP>Vv+TzBS2`rRPCP0SCj6Zs~P9I2sw7=XJYk9UUA6(0uj5@@KXXi+o~ z&Q8Dr2Kc48im1rN_Xkg%qhEz0rjSQGt~7td`E^;F%%%Q^sq!`M413+kOYA|Q;BzNY z#<8@!T%o*8&`Xk#iKcwJZyd{RGLd_x7^)2~GZSxGNt(cs*ar)cY{F7R5wH*NunoLR ze*mIQ*;=&`SFHf6@K}nrP2W%E?h>3Kop;Q^tP6n@k2-bROM~I6{uRgy@u|Sy&*^MP z_J6Yc%n}NxZZ|76=;_|AMQw_+o7p9{rEz>ymTy%yqZ)wx0L(A z%o|u44(t>1DE+I=#!}O9?BUaZ?GYIqdYm2wIu&|AVuyM~0)%?L`YA_P!+Oy=`Guk2 zTg-3VE*d@13e?pIwqUZQpUf!=D{0ON$ ztxc3K;4+5tm=b1^4;&P^$YwC=t{?nE_C{1Sf)8hS6sV&zPn9y2mXvQ|eL%RUEHHhY zJ~@BL9a?pWL%}dNXnaH+3KQu@J8~aiD9#YQPs&}9Got2Ka=j1eJAgmsiW;=4!3n{G zF7b7J=uk4|PPgyhzQ}QSmjUJLdCr=o%m0G%-4zcc0KRGzUwYr+u!9b-vZ&IBUUl9M-O3Qko;iSEZuSg6i%EageC>p7THyGschfg3)yb&G7rVkhP-pUQM$wN#< z`XXJ|UsymGVs#UZC!Pj0bS>u|1~`===^1n}kGM0Er)K4x$hllf^lWlk9qH@IHE9$@ zD95=IIk`(;{?>k(W(1?JW^oyoqfw8%O=EY=AkYRRG+uWVwuM@#S-DSXs^e~1;pyyt zDC=)vizhv(5&iLeGdbyax@Eb5w^qjk$y!`d9TPMO+bIus&#!%LG zM;}$ZWB4jXo6t-DbHMCXrEf)r`*RtjHbs*Nlk%uJ7l`XAhI;x_H3x>t6fcm>cNRE^ zg1k(T+Vc_bc2r{RtN=PnfchoZ2Is2+w+c*nyf5H57rj=NX<5jf9?uRHCjqWt7;wXO zbX-w!0j6MSS7qKV2i}Dkst~h?)e;bQdKzIjt9P1bNHWrD(KDYL6;(W`_~!i{Y8+zkq!{ z5OWdWw=Wt%9PEw3Cgk6O$K-{%1QY4C#0%m8M}q{)&z7t=?W|ngwy$exr$MZLIMG9f z6uRu9ARR4X0hpI&`3^vy)N}8!w{?M=^gIde?NZ^J7k8l<@MZ81EnRl}-Udub(kLWg~49+Z~YnG1l&%dSNKwT@0Fp3$#~Vd*`*d zlms}cICHi&-M1i)7#gCS5wKB<3&C0j0!SKbOA*ZDHLDQs*CLAT2i~RPnOT@c!_wC; zYC6J$<5*ZkHX17|)*QLW0RQIwK6xr98KNqFVl5cyGu+$)0SM<0w&zhR?E@ZF`HNHn zJ<D5-e&MV7J09 z*88YnK@+9*uaMd>j}PBj(eZz21X$9K)ZDbaMcx@*GtL50eb6^+TNa_6@y~_$#4#`&uZA3@85j7Jb#kFOh}{vkotQxL;TVtA?3KsoYD zuN#~pir)Q^bQ{B06U-!wjjaIiF-DEaWrpv5YIsydfaII>t91<-Ux18IcVwhWer+r> zre$ergnl#U)x%&gGL=H>A8~fHzo$Xnd1`I^k`5(f9@ES%l3~fjkD!-vxWtH%t}Tex zpmqS)((Yf=d#d_m*(&l|P%^vvUu1ml10c3C?_1Z~%*Smus9n?5+nrjQ&s zF6YQQ-uEc_{Wd3*#&^vL@nSyx$;CL#Bdxy^b8=eNw}^LDnW&a&I|YD z{%PMAd)wGHS3p)zA)jJ?GmqBSng8{_o&CTq%v^)ioUKRC_xW|)BgXt#|XTf&fORC<5Q14rQm z8A?Gvt=ac#bVk?iJJTOR=7#AN% zQc^NUqyBK_1pDc86Qfo0_OI1!j+VMuN9UbW#Buc23&kK4-!DWdp$Q_=ie`hT-cQwh zyk#G5brO;vftLrq%DJ3L3*V#8WTx_)n-0In?q!~|5UK`_GLG(Sb7LHTp59Kjh^eXh zbW-|kZ+qq9Q$&zBP?Z^?5(En>Z-=wMuK`}nYVVUzDB^VSv(2MqchTbJ{X@wxHt`ev z5x=hFR`6z{P?}fg9qe(cE9G?kQPS3WstaX4oWO$J+=t6IY^@cC5l@DSF}?_2!W-+3 zA6Deer*luhWctv)%+DivtG9Ke_}kkI*O7*wlZ`q4lkpZNmob5Z@OxfQ<8)9lNVEYP zw;0}!IQ2JvkcZ(Vz)wniLW#R;J1w*E@``e+zc{XTH0-r(B}iIc%OD<8hvly~aT5A1 zIIbe#JxAE)(_TNUwMoM?e5Dq9KB7^A&_|(YRvH!XU^?V82t>Ux%~<6+&A87Yw-he^ zcIVk(VZmZ{N2kz`m(y$oMaP2$!$&!CMW&4n$LGeZrh!^>ROLM_sIrPF*U_)e_#Yos}ba)=a*H-?H7=9(dFo5&(h8L-u=`skEKR zsho$ooF#4i0>#es{9h2vpP_$yTI8H)KwC1$coPglEg5 zf{z750)W>eVXW^Q%mX}^zBkM8$eoBmS1#iQG z^-+yx=|?K*XT)ot=zSKSke_yB@4ZXkMovj^v{%EZHH7EK~U|&a+pmx@yxJ1wZsTel(aW(!acH_48{I z-(>PHZ(O@^vcKQn`HJQyW}q#?@d{9v34{e(JpTctMt)-s%qm|8Po_@Z~>*Q!q zlHl5N@0a?RgkJxIqFzK<1AQsVNu7uH?aGjS+7~u_O;79QXZg`Q!;j?^)w!*X*+oI) zPm;VY7HOCcP zjif($BPvdvYIRk7P4)a#j!U8ErzMJs`y9(<9a3++@0>jD(r;3_8Dw-lb4<-{qH?(V zf0;loCYNxV27pbT=Y-AOgBML#*S6)w<5`gY4pM;!{4LckHFu)B_%pU}TV}fco_thK zooCp89{hd?E5lks*iD}9gt-PtZLrRAFc)^tfKFZDCM@kft$eG?&)-I~Ikne+VsYsL zF{w8`FlOG=WG$V>R6#B|?mb^IaMWGT{BUumt(R+ONB0&8r#k2J(JbFRd%gOnXt*Jy zTUlyZ2J-XHoy!S0`$xv#hdv96)ETC*;48O=Aq{TgnVOcLHsa+|y$k+Totvv-+i(2U ztVs~G2CA}8pUDnAxylS4EkBcoITFO5=g0OiD2DLtI&aXa@)RAhG&LXFj1MN)?C?>uXzX?2pyNo7(ecm z-F#@?Yp&A8U9`4G^9Hc^6_4DNS+V2zW;=!}CMLqLGC_F6nPq67j>-EKpy$QmdGR0X zx;{o#j*mekJ_rlgumtBaE&MWxI}khhnTC=pF z!Ax5rudcenJyak3;SEl7um@TYeHMe{<#xYOIAY&bMg`EksG&J%!Pu7mxUFEMhwCOp zw`nnn*MC_&Ma9XD?W{4txBWCCKPUET)rpu>lv36ry#vo@jSl5Kejm7E88N?ZRGjau z+uHzt+N{aKH(shfc^r!ZAy(s4kHe4TuB`~LSY;P^Urk)r$CYoH{4%_TZ*?lbxx>1B zkzNMNU2wx4^=gze>Y-U4K@PFXZlg$=dYofae5lrCrI^dXJE@jb{nLk71l4%5chHr+ z^I<-Jd&#U?XH})3KbK0|vsPa$Ut~LPYCnz*(DvBOxR+x8zg^w}V;gOB#{Lt7m`>tu z8pX4}T)!IBuK~UwsX;R=@5-9he^^n?KPG)rW^HQbUTO?^-Xz7Q(jaguw0uh|qm7$o z)|&pdcY(7>XsXi!?o-VBT;Uxdre7dPrQp-?>}3^BL)~I3Z`22;0!mz4@kf{RmE4tA z+dh-tY44o83qE@I+q|3E#AMo8+?iD?SToZ3a-Rq^+b`!@IeI}3WEy(z)f&JvnpfFP z6iLm7KX5DJy2l}zt-o%4IBxSRT##OQyrzOcDfImspro=Au8S?7Sje6D(eG$13rQai_jeM)Lyb`Lr5q`#`N1 z@0B~uWHUF?eWUFz*IeHGt-bCMsctw;(KAHBe~|#t;&DY`>m?RLuN!-C*5A^qOTg%c z+92htm~@{?0^fYVKHn@vljOV;dDr2U+B@Binfu(=C?mI4JgrYb4@9zVNJF42^U3J` z&cd~BA%38rFJE%dE#=tmYMQYk>AykYnoR!Yr4XQJi^sAQ%;CS3g^d<2B0Z?-kbi~? z1%ky#ujo)gWNYKq#kT;aPLFL)#pHfBH)ALVf1dHh&8s8~>S*x_8NLjJv)x*w#-U|> zQD)N=0Q~_|I~myW%o8W6xmWjE!k8_uH32Odp_zov%;&Sg-XTB-OBHzrIxPw2$PQ^M z+yqwg8Zpva*p{&taaQy$GLv;ThpJai4}Ev^sqb43IP0Pz;5yWChuzJD?jLdL6Vw4C ztpJ%oDNeI5=3M)Xh_ra>E&g434X`v-?ECeOzW)MA!K@kK&N~LRP5Kux8 z5NQSwkS-ad8B#h15s*e5Iz*+rJBAt&5Rh(=&Y?SIeiwR&KJW8>?{R$pc?ev))?Vve z=ZfuY%@Ps1^7UZ+t_R9?SlFBAHYez`q!6h7Wq9*}#POyYIO2+lCWb8MKPy zk;!XA%2X8n?@*Nw_Qoa)oK6hy4Yt2X=~o0inxqI{mmWp%>U3hMc+R*?e5ZlK^TPJ1eqHayIz3`DqZE4HYo?AN`wZ=szDk|i<;06#CvjSIuqa^D#~gVP?!`ktyS zIto$&g}m#ywd$(`vjv>4^!M!raH-afHE}+}u6b&zboNRSj+u;xUtW|8a z=xyL#vpZ4UlNFk!7keB=%=fCP0CH=+=Ep+l_SSS9yn5og>Uz2|{b6924Sh7r)HXIr zgd|;zO7nJ*zA_2^r?^guG_8uzA`X$v&4`MM;XLSV^@_^2Gx0aiptI9gy7?jt=HyFN zME3Jr-tK%$^j^gu`f|0kRc{`Se}wecVJU;aLeu^n8UcILF`wW z70-|Ps0Szz1k!r8C1s=wc3(ZN<>-uvZA8|U9AUj)QI&PLkAG(+02TqFFpzBq$#?>Sm5RKFL z_U$Dj72cKbrGj5O>3k|vX?rT=xr9D8yNU=8x7qESm4cb(mKn8@A^12=r(?41J1K)J z92gG9N6M2kvY({JMJWl1i9I_sb*nF^#-wq2E4!VB;eW5N%uCd&h;T0PCOJ>fk9tIH z1|$*0EgkS7`i#Q6dibJbZE=9?Nq-l<3CGPDNzgr^ z=(uP2I_%%VGs)Q^`C@As_HmMS69G2bA!o9R2ddE@yPVV@GB#$&Z`X1nqZP@)2oMuyDYbX6B)0|X5;E&xD8YBkd#^jIK;p}bbmCE@t@ zjxUW<9IzIhvm*%%ruaUvl}4!73JO3CQk9ssx9rHuwb^(5zm7&Rp1#0}+0Uxe2>lvg@wt|0u#8teb8U zVOMzF2Q{<;;K~o#M30(oy3G0Mv4K$SOv(&}N5ditE@JU}tuZ@2f-)=X?ZJvj@la1w zay{4xhMvdkQ%SO&->sO0mziaI4RL{n%lji!q0zFX(Ei>xOTLojiikZ9Z zNZeJO!qf_$%$Gw(Z4?5dKSgM~1D+zSedHbfd zUycZqG9JpP9}I6F;SHHuWNN&2y1*oz>=GsJ`MIC$kb#ebp<6yBWej}AUU8q@^t+4t zF{}7F5*uzJ1e`i@kYiX2+0J3gXBFW zWxD=WWarSBbXi5fAU9nEf(aeUA z+*og- z)Cm(iH)M56aU4YRg1e9;VQi|eg6Fm%d407o3M$Arx>rWsHIXsWo+XuSZqzpYAy^=a zm$|4loaEhYMC#M!e&y#W50^{I2c_GhPoEjEL{{T_K9W+MK3R2qaO`P1Mj(BlTCPLikmWC^lxdo4H%=Kv zpY&ysVT^~FFgCb!L$GiX^$Q`+Qy`9z_Orn(fu7T+Q#0m1i{{OWhCA=K(s{hw!|{P) zkizQ41g6@ccIhOrZWFoqHO(VCGq~ ztSp0gB|!In!)q6lK#$grhU8eLt6_c5L-fYSyWIv~lmcZ0=GOT{TwKn{`p3Ow9GKw|K$D`cfQ){E?K(V6Qiai``W^H*yHwcPslO>df~Q+8^$)|U`;q8ry$ z@;-pgej$exx-Yl@2mS3+vBTt#ax9=nQi=M(U7e*?Xp3(Xq=W}p_vOkd59_0g8jXu) z1#ddJYdJ4KI@81!0HlapwE^)Sa0dsdYMSB4f}IBF90x*;hFoaI&Jzmf30di=9L4uq!2 zPqh|$`<^x!J)m+PwU#o=o&**Mmg{`%=cv)}xXaG3hXDBHM{ml+FPMDgC%;BlPX-?d z*HgWYOFBrLaab9m|WtRSo)9%2ZfDHhS0cb{*Xjl^&? zf%la9P?I)C;?n_N^Ugb^;OH|corKPhsEi#|AGx(rCXXkedsShOo#hsq)f2ebsSVxy z(BGTb|L-O)bAgDju_)0VHC}7hLYxfRU)WK6p1OM{$<9MMws)W2h2$cC*Qx%Uv5xB; z_xO2rg1CgsJL9j5aLwB!u@Ws{KMfXd%y^=Y(rT8s+wGk(ypw0Zlfvx~^cb)!+(7Z* z$?}`PTxHMYX6%JQ3R*J4M|)ZJ+k;xQGo6iMlwb3N*A6J1U|{u%{B_2ze5>i=0odSZ zW-{~C{j#4D_FWG=1?&qUz|~)~MiWKhmZBvAbrIA~O5ws6e@iO>MryO_;PmKs{f4Ve za3o^-F>XQ^IDuGspa5%8$fLkd%`VG?oX23){7O8|(x{KgMx=f08i z?`N||4hQ1Y{c>xO>K&#>m%ASf>%->(riOlzJu>i>|MSok1I{x+mR0WcsZU~U7XGHI zyp>9@+2}6Q?T)8pfk2kXLuD=kUQ)o1F(rnR(7Wq?Sx4s_CqM00zX8Poj-~X6=(ydt zb6%xU@<~}XTaD9*baZaF7>aW{;}cof7;wd>0C#Wf;H)X*o+WNb`(+~S?U&zS{&ia3 z?xt;vW_u~3i`-|5#zMhH$Co48=eBl9R+3AYI)7Y$lBlwclf)Mh8+aQ#zVQcNPxMcW7^ff!WZR`VPn)|OPNWX zTKYq?{Fq@SvQ(b><>)^Le@_maA#?z5*E#E+|JKU%_=Yd|x&1oimZTbgh&(gHgZjM~ zhu0JCGSv>x`@_&32DQV9SNhD2DGZ!FDjX8JWY*a+yx(o<43i2bRu4F!ZEg&g=^;C( z3RM1N3*l%r7c$!8jfr?@hK^4!LUzCB($>VlU&_)WwEa_5;D`dRFunwn9}wZ1 z!REaQ2w)pG=)oi26=Z3YlH`b zzVkN%Yl%*Yr3!u8>sXKK3LkzN0U8ri812SUaDpwD8CG=9y(5gSJ(P{?UI=!QHstn4Uv^qqB}FCcxDF&<&C=*Zv|KQ}UhC0Sn{E&)RGi znr}m@6e+23_UkZ7+rIm-c@puyp>cL7#>!AZ?(Dhiqbhg5KT$-4Mt1nmcPFdf)Ar0R zSy4{{{0~p9F|nKljjDW!>^<3;!v2^Qu&4DGAtwu?M`B|W{b9X2s>24gOTCMF$RP(% zZtn7N?PJ29eg87ty!hNj4~|j<4aSGjw%db0`K<&n5ltC14CUgrtX&u721HH#|{+LUC z+=Sbg(B@Zm-Uu1dse`j$e+Zz}9x+PX>IeFVVzajm05Ic49nKl4t-$2@$Cf|t$cTd~piH_q}O~!3Gy^ROu;y*tHfQk84)su02#UOzhytY|0ySo5j~#vvDNXOVbAYCToD z?8z|ttq{J|o9IGB_IN}9!4^Bnx@oE?yFbh)%^)IP8$7xKG`-a4YEtp|qU=#=0N%?xTY+WJF*W$=zl>63Epby5um)!?iL^iz~NnU*$moD zIJC`i?Xhx@c0*0(?f1+Y-k|^m$BsMplUEdS8oHVv+m?pvLmdS1>6`N>88Gb$$vn#} zhFQDqI$yF!*w)wTIA-qP!}Jd2h1;;WK%#4)kWbAcjf7`jg@zdh$P0?W z__O!R5B(;VYepkZZF<+$Z$1Wx3cBo<`HWr~;oGkF05VA8-$6=5_7;0_Fk5}Hd!XVZ zKp77q({#ZHw#ov^<(IU4g|xe7Ap$ya2ez6Nx~|ZE%Tgg#o3FDL#;E0zQEAn-Xwkj; zuFN85(=rosr{GQhSak0P^+RIg5}G0y5@vJ07Lpx-@)f)qeIoOQ)=0`b{*6RsdIaOj z=SM&pfPZ?vcysc$87ehQbv>GxzqL%^j6R>c^dqMp_sjBN&U>TWG&vA8>@1)452sf= z3U5pPLh+<<4&VL=^lSUW2X<`k$EO06lR+w+EWqu4z^kJZRFp-EpM1bWuA<|=Co}0W zJD<)q%S7ScC?8vPhixH_vuq*IWLJKH7G)c_o7g;)l5-mL)Rr##VG*ot0FfTox1SfS z6(CJ+swQc%nX#(Q(l8)6rnF!__mR4cmZ-nGKq;miP3GzAtS8kJw-B&1G@Br7*|FG7 zPszhK$wR`M&D*Yp?^Lg)ZEIswg--bUzRBXk8q>C8QqfLTYaJA|EN`JqG5CN0a)5Fp zH66dW<{=r#DfrKoYbvVZ!?oV9^mSZb;g0f4pf1-INbh{h6G76e8fsKEE`TqnUkja- z#n!nwg7^ZHI@b$fYcFLrs&r`#CYv=g9ayP93V@jyi5zAhBx>k!TAzD{wg-9D4fV-j zW>fDHIAud>)qO~g#^_)WdRk&)J&(fg{%=V=X8zjTaERj&P$Aa-Fyq|ph|*~9yZ!J2 zCBNvWZ;^A>o1FE*Pi<{Hj2-)~L?4}4d|1W<9C!`zB9wbmfpLrb;o23Q)^!LuQho*@ ztaCiuOqyJb2=Rdeo@6(#Ue=Sc{$5v% zF%i`;YY5(QFCpvZ;$ns_3CVVP1BfTv?j_^5J?oNl$2KOh?~5))W&4$;6}FdrYUm*S zUmooC#wdwp>gTIF)oMpU_&RgJug^+OKG@l;)JtXsyh!}`>ps3Eh3_&TUr-Z;xlXy8 zzsI#}W|KwxRBh#HMX^LRcTSy~hugNg60*yiVVy!{k`FseRKLn&&`g7A)kHpg&b5{@MzRznhRQp{`%V(!L;?@RM$Tgsg;q{G z$jS%Fqb`Q>SbW`bh}b{g@h{;ZMi4ryeWW<)hVZ=;dl>4fCi;%*1^@*f2FZuTJJWLK z)~OaMVAQ~BVD8KPA@Xa(^L9UR&PN6ivpq3T$12e}&zN%7eODUXL03}PxUF)Evfp)W z(EjigPyDqzj+>`XUTB<+zcflSE;Ld1=GyC1Zm$`c7=SId9`pR!wRgIHT}Y zWc?eu2XBhLUS!%aef&+K3tyJtGz5A8Fw=v|Yn8wa^MyYA6OMDk7aue47Wvd0exxx^ zd%u9{HhE+YoVKy>s4{IBi_KGtQSHK*eqEFZqxgjb9;dhrc_N8^aS9*fsIctNfNuR57kA0g=pdt499uiGgU5E}D=9ew_NcO>`R53xjDm+0R)O0rGBnymjGFH1^4EaNEryH4Xj zTNp71A=ExU(LzGbj%zs!)i@8kILHu4bMC64D%cUEB@)914j1pg^^850>s2~h7GvFEHU`m!b}$D;ZG-WUCS1(YatQZ z_fgFm6VW`nqsX3&D|m=8b33FscTmQ+oo&2e+Vixq63daEm7D-)>5oXJQ`aQGZn4>{ zv%1Y7M9}wjrw*bIqEA{+*7~^yH1;*}p`=W9?dqrP-s&f8#Zmi(0g6+%ooc9rJ1~?q zo26Tvg^r?YrwWESXE_AywAXW6dsxwgd%E|qIuc`*VtYw~4T8^+Kj|L;L*dTcjJscl zsh*U92)|2sA`A*u(Eqv*iJt5XG_?J89aHr^SLvhv2$PTg87LI0uN`_Cbi06D_EVZO z&|N#sMdMM(R$Ul7ZL+c*-HDI%y$hpYf|^zXYm7+^@;b4E=C3|Gp6u#Fe1c+k6qczT zJ*Ys16@~2&qvJ@9TqE44;t~B~L2&#pHfOdBUbpPjY)6s?KJg8|qy%!a4lCX9^`Xyg zKB2Hwm|U0-DE>9SqvtBNkaa)`gE}Pr1CTZ&E}yJ+fWD7o6gf;j)fU+}x-A)3n4rxW zK*3?+Xho)Nbz?7ObT{on`zA+XZ^*|933ezFO--V}yD(5c1oF)<^w;v&&cZL?8;m7I=eY{W%kAsf{HWczW|wQ4lNX|stgmDGpie2_R@6aAXSx(B zHXtU!MY|LwzFfGPdFHtK%4j49sY)3hYm^f<#bm?P zvP1viO`HHl8q4oRl`d8!cGIg4egI92gEC{+E9`0jMx-aE_wXGU^5REh5T zIyW?gnGNv7f7oM#^bxbCI$dx^|>IRjh1j(mqtzf(L zGn@X$_FtB`w~9eRgSS9mG4nBiZZzFQm&ypvYQ4R^rQs?t%ATm`%qLYT=$`O4WNl$7 zrd_AVH)@hu#)|ORx$87zhxr=kmi7z&jYdB%w4Bk=8a%72D*mWMC>HG^ZM_l-N?h=x z?X;Zg)#`{Ve*cEiLlb*LhPnw&jo%^Gw-D>%GKQbn(3-nPLWH`FH9R74CrK1S8EV8{ z-<8{y+}JlH-kDwkTe(kY1t~1ji?IIqUh6YKnaBfk>Y%s*r#vtv$2-Idw&=T&=n zJi4qN$o$xfF#nORz$;T3_q)a2x7{(nJkG1$W266}1%M=)C`>ySOO!$I5t)RS#uRE9 ze#x2dIr+hkL57c~W@$8U`JJlIJnsN;_z*5;s(mYM@q5h`lOo55MUp>c9--0car3tg zuvtOWYGCC{#cnMG!*688j*}lW)6Vb(t#bgEK~I@AAjN086Hv-VugG4E8C>U&yoyca zO=Ob|Zzip)%09E1Xs$nqtPYOG5nSCi`Ny38hv;4+hO<>9gFAj5?JpbZgY#hffxCY) zIxJB7L&gEB93wOaC~be|_#R zD@vxlts!dIz_)982FmuX<3bw*=yTaB`aHDlp#5uSiF5Hsse-m4|JyWufo@P|Dz?I( z))iU{FU{sU-i<>G82T_&0ik?T?lX|(z}#{O7nzwcN^s$gU4f=e4&Pmjo376G6_TIZ zOH^C~Y^={yuwxiEfUyYKKrC^9uetzp?7W`7)mP5~Ad(bq?zF|XEK}wO0XQb(m?|t@ zo>l_?a}ld3?-%1XQz)VLKDvh|JV1U?n2~R^fQ07@r`ucqp{o8@n^}=X9@??z$0$zx zA!*TUBR>1~XZ(rqgmV#goA|i9*N5uSp?-r`x6OAurX4j9zVDmNGu@jEni?^$Wik+HkH`0>b-vO#h}I_1T{lyi#! ztqKJLn`A8_0|Bdp&7sfvk*g^pJ%4lN@^%|PjOKeY|w~@ z3IPj-l_^K&`{L6FT(i-r)G?(q&x+C)f*g6C=M*@jJ&N@4A`r^?f;#F;f0{2T9CZxL z4pIP^uT?*Wr}&P=S*ti=+}|FCTR)-a-qX?5Ny{f#YS0q8Bq%`AL%SV{ED?3rzVh-G zym)d1p%?IiR^tqGu7Sq&{Xzd2Td|ewygTyMht>4bOlp z_O4^tPYF3c=klHic!Z@ef&G zQf(T*OpD;y-C_Y@y#v(j;JDy)SiLJ|oA~YUhM{<)%GBy*X)1ZW;+YA6sB)CC+jmo# zz1%UiDD^-?jIk_dOAru%xbGTZY1bhdG-JmNq>1*pEwiYn@>zuo_auMw#i}hx;TZ(~ zp*D#&eD4G`RAitrQ*8H@W|>odgi2WvA_cyYzi&5*Tm|X-aC4g+qr}HIY{f@p*#jmS zHp(EWbv!5*Wlp?G&Nx$pn8%jjV8>L=eKSl1elqfzix%#0_(MMe< zQMMkNq6D?;Mj z&;~o)U}UNjEg6g5LP^>%lKIQU!j?F@GBLy{1z9&W6OeO=X&<@lj~H>U*7zp z06(gTc>d?ygn3WL5>7ClnNM*W>UWwf7GiI~w-en?RGrnmwq0inUjxlMem+C?Be=Do z6%F}|Z+@TkJOhxDklK@LpSH6YeToc2C74gtrd#1{)8UZ5V52X3SZRxZT728}>Nd|% zeWZ{K#cMIt`X3*0^p9I;UAMLdzC=0OO$?7u3Y#AB`igHPMEojPw%;3(zKYC@OCq+Z z?I8*MW<9SxBGZMY4*Mc@_ld!heqee&Om8%UO{8x%(z)xB_t52ftsB4MQ9xzk=gVl= zrUT=HH#-5=3~5dAutST4fO5atC5y%Kpu*d;SDVq)55CK8z?j=v?GmmH4q9h67w%-# z)?Erg0~1YJl{TSQGv3y+yFQemFTW{;@vitu9a<_ z*1`y#Vh!)QN4dqimgW+%JQXcz1iw#IHfYQ@{<1gpyhV|=_t$&}rKNpwocWWCIP*$; z+X4s(6Ep0G(b4foweByn&d^IQh$m}Tyba~J+@hzi?eE%jD~|Rastpa5a9*3Gav6Vp zKCT~w{GY5aDw&Q79`oT|;7D>GJ&H?&IARN-mc?;hjdo++$0F=X0oVKIrtVAQe5I&A zB2j#pr#+z$(;*!{bBL5S2-IIrs>(+FNCp4UDt z3t?-cUdviT4YpvP-q+im*<QrWduw76WM4bgFv+E5a>+g&<{ z=I_L^e-=R;+xOuiy@->ZnF0l_>#daVZUX5 z0yvB?dyhWnw0*QP4n@a@pAE3oH;v;|fn$&Dly7N>A>b|UXV2W{9|RXdGj`a|fv3hw zIB$CKwxg-lF@(3&Gp6-N%6!v?jjMQ0$5yUcv&0KLNgQt~8Jdm|Un^{3Nx@Q%8Xvp$ z0==Ovef9x0#R^iY-uvw`{4Hb2j`N#u@OwO@fjC#OP}<_thOK21`qFC6W)}Qs6Lt8Q zj!j2N?5J)2hxoZlEQwE6>|75!npq?gKIgHk7?2C{Ks&4LxH@mqcN-5V4!$`TDL_** zWOSTld3>a0e^6}ss=2UF=1v;5iQetn9h|7X2xZE-1Cj^ifP-n>a-!9;%eypMSdW!M z$E%qd?t>~A*Xe=Y3;Ft4-}ld*%uU&?o%!(~h3k0bus#ovIC;`5BDCwPliK~2_m$jM zRHOh@u8&3Bs64{A=lx`aa(z0R_fYJv@Gp)3C*wnJcA=*|2Ki~J8Zh}iv32tk1^|0; z_{!B}(IBSIGdJm3mM0})t5DZw$nf=S;rrIh{R|bDAqdR{4yw?0LaURF-Dq8>?S12pqeqgknZZr>$(BZx;&MNPncS%oLSDU+4AzuGIyo1Zz z89X{XRGf&R_gcJCxDIZ0RV4|)0gBTltq4D*T!fQuegBQaVq$k|H##?~TKG~JTlaEP z{$erjV9Qkps(B5N61OuYu6RIq2Mq?;5VpD%|H?<%vo!xr!?zPdVn|90^C_qg8n;q6 zVYTyUe8c`_nKLH38~Y}<9OUW{wal5NYIXp*RX;xLtt$!=jk)bKnKg>wZ>kB+z5t=$ zGn0Q;W5IY4Ub9!4q2hH7^X5FF>A<1!;{-?pd>m_qp0t0DTu|ly8;YsQo94YsrA54I ztpwQnCYXo{n7Vfvx6yl}3-nILdk2w}P`NvwS-ivI_q$BYtx zeVkD|crm$hy+i|^Iy)UR)dTlI7&fcms{24;R{<~(XDRDx-Kscal{fZb0&dR5l%H-a z1se%B72cQgT&suR{>sh)%G<~;a;XG=>W=q`&eHS?hqs zZxpz*Gw4%g_f4^3rVyt0Y@wIgI|hEH$tL6;+i>>+m2m;6_4Nx{GN1AuKT3=)LN2Mf zaEOPoC zEPfddHuhzx=+T-IU7$#$#-tZNL%z-gJEa_XW{oA7yp1*RB<)&Ak;a5wN4RdK9l)w_ z%7tWqj)+c?x}xSB?IT}Cu!flWdgxT9IVu z;$*HkZ>O^OwP97G2z=R+NIXQvf!#jsFNM&MLdhFn-nT(5(j2U%297Bth3iEMHUQNx z1)YKl|B&~ugW3>ouKUpt$A^HkS^M@%Fj9A7fw4_W6cu^Xby%#>tRL&W0Jq1Y1M}Oj ziW9|PUe&g}OK0r(>#H9tPX?;w+F2-bN>-#3lHinGhg z+X}q#Pp*FB1T5Fz5s{amSUefS@$Z=L=B)Gu0(cAJ2H%_m;% zirtfg7h@E}eLIt0mu|{BlP1*_W8J+O?;Cai7QJ42n2;MdK*gX*F_G04`48m&{hAgC zz1S+R$BTfC*}X|b>>}T%2yglWWS6I0GFX$w(|j8}bhK7p-_0MFV zu*P8M$Fo08&rhgNBWheprvCsyHIBv{XRd9uUfn7sdy&^-#ohFRWBIui#%wE|j?yx? z?EX^X=zJOh9Fm*M@5Q(?ZkZ>ETRkEGdc<4e0}Kq^sBD1IYFjlDF~oB#b1Qcr|7jW$ z(Htevls&O%8?DtWV;Z*XNYKhYFnzMZcbK5=B*g~eXtaEB&KB?+lKB~Q*r+-PU3znq zrkV?Q5JbW&AW@qDn4)tpDLJKkKf^VwR}z#rf%jj+XSz)^0J1>NnxeW{vn(E&ZF|@3 z(g7lLQW}(B6FD{7g6gn;7dMVTJ3|K_p~LF&X6UfNU0Y#$O|Jk- zbn6a)9VU?ETW6utMGQA=>YBMiy}g^*-Z&{$T z8Blx3XAt&<%`&C*$Y;98T5#2ovJoNi7&e6Ksn?|#1Ue$6gm$x>x|nCi*SQBvgO^QW zbd9C*_vtYDi$5RR4(IWrTX$cq*h;nY5}G(0ot)vIJzDFyzOX<+rPm5{&dwH&(*IhA zqrxL9?Kb!ML;bAk`}2>U{z})}O+Rh{5UC z5wAUoC6l4wl%EwSpR@SziKQmfIOP<7Q)fsgn4R*d)QbR}y>)JoEMAUI`#NC`)U5dZ2g(|f+0eB8(3cfYFzYPdZ z?og%xHJxQ>Ky`-dE%#uN7&g#q9F6bv4FM$Y=l^CtD8qGqaK>z2%p{VS-ap>C+P5NL z>%_t7d9G6T{V>!8ECw|#j5ZGh8dX0(%y<|q^=MaY0R6`zE@96lyH3Re)kIrEnKqqL zbG0o^Dxxf2+j(uz0FD}{A!Lc;u4@OJC6YGf0UU6Kvn(*{@u}@tSxMC`JKK!Y%Ec6( zZnCDCh1_#{qj@r&()gy}eo{?EBZ<{=D?5lsaQlt*X=Lg;qXEV4GH=LRhP-^E&*PSf z3SA_q0kbDkF-Os;J9ElxpyAa`d++xx+ftKZ#-ER=S}U6mH_cT!@mCn*l)ox zyUT)7=)svtf=p3My`$#%$bzJ&x&Y07O>Dor+0Re#4xYH{t7*UXvW=gyX;oXgR*V0| z90BzA0q_{Xb>8erCCE-Nj~MgWnyc;gSbcY>&^cFOR?<_Tfm>_sd5j(&2M+LT*pY2_ zol|;T635SQvVAIzI4ek4h5{=@{tb`?5ESP%51-aa4U)ejb>O^&uR=)s$o`&M%G0U6 z$e#SIlSna^v(B{?8t#WXTU$+;kA#y>%fh!*SUwOzqX1It(LW#khug{f9UcL(?~7bD zW(PiCwk&WMx=t%#7rhWD=Pg<7xm2iU*F;_#OR)^YFXjcf|J9u3Jp7R*(c+xdLy&?UlL&xNcxlq3He8|WOT8bE;N}wd~&7=|xk6Q!ytK{ptr&;*~YBP#`E*u)LeWjJL9y}=TuVBo* zu1zp{xW$MRZ9Khy!N+@bB8>>!s4%nkVO(^C?ro_x94D_hw>l&AK^2u;YxAaG<8s$5ck`^8iKD3SMsLV0R zlRq}Mefte&?dw)S&omJmOH@MP`Pb-~c@{DHiK7#(H(Le$YfLeCfP+~nUHQ9cW*JtK>JY{eQd2 zA3Fpn)FnBn_PNXjzjhzqxVYofaj`b{6OBvHR20}FxmQY)_F%?$=OT4gUd!-Wj`^L} zKKLr=zCZnflij%^dmQ1&r)3EbW7j`)@*H1yiKD`t&-lRu@?bS3WmUeW-O?zq**4uO zzzFqh@Tk`Gv39)H=PoJ>-eC6vnEoRS?}5D0>Tj;lt=3z>6mvr1MI&EJJzXE;(9scD z%rzPDMDb3HzMSyle;BG)y*yg?*|X%M4DSHTqo@AP`tVr1^tgdvrV->lUq=6Y?Zp_v zD;x?L>xRN3pjz|l&`K+sVsTL5HDl9Nqmk@z{$X*RC#Zq)bk1xCqt5n~5F=s!TUT8H~30Q^X^7hm6MrV}XsR_3vJNN1}4{QOa6r5wH} z=Gg?91P||Z!Bf{6;7Ec(TNxD#3HU0@K6xu4OVQoz-2GfG$ zLy4*Gs}R8oapViHbf8a_*@c@kf)$LT^p};Q6<>$C;w2KkEpu?_kIcYh+PQT&U#x!N z!W|(e@lJ>Tmc*hze3ATwo^n$ew$r2-$zObT8du#}Ii}j4Wl8B8@Rreh3(pTyzy-tZ4I7}!;c>yCf^iD%SV%QokJCS5 zM;|k26H^&BjNLqxHhlLW@lKMElJR~x^P7QOP*4n2ed~*>2b;5>1dsKfWh4LaKLuzq)e z{j?c=vhDVQM386ym7!sJ%t`!@&i>61VE5Ag3=>qTM$(ePfs&(3$h0Rw-PCgAzj4^# z;NU->`QOu%n1YxA&ZmpufHHWukzq$0^36FF*luk8wNz$ku5%&GJj*VpXFc=PpM=oQ zzd}lD`q=WM0p$T(_#U250NM3 zqx;RLF{b3EDO%^|*rW5QZ*!t{B}VFnP4&X-HX3V&5hPC|*`_b)@?NKBLs-XG-fW#W ztLyi~sVXj3-L>AS{41yEm#`~vR|Q};j$Zd1OKLt6W}RZwnq3HsG%lMw3i?u4dhi$N zf)@E=Ho}RS&g)23JmF+HM_3JS%XW?;^g_MhcEavC0~*00A+RqQ2SY(EKvXrO=4Z6DmjNgh|hJjwI5Id0WOUD5Dok5WwpP)S%Woq`%6bjQRzGz|BO%&)an1l%$6uePu6W$x!<=tM@0(QjFIvAVxcU!Rm#&W-u(VoSs({5- zTox6VI2Ac87ft;>LabbOZ`XBf5`~G-4mdfyTS-=n7ks}Mn%L(o^-EoaL+iD0%YA`n z(jWgyEs+0teV#gga-I?a`riqBzitRK z1-qSt`o__1kW+1F{dOenLBTrd9h-*|eP58{?o?3#J<~4x-0#_k5{tsy9-;E%G4Y82 zKlK&FgMzNvj^~VBCBf2Eyy=F*x)^(CY3E3AaN|`HMt6-M$d9q z#_L*hy~>dGyomS7QyiS*{!D-8xBs#c&-AjeBVcE2!8T1-77~38S_3(w{z1&lk&uZx za8gE*?#X4f!UQ^9$GuzdJ0pnDH&gSezE7RmJN2@2Z(ZLm^^%_G5Ol&H5eT`@VpG!< zayk+beP1&x>t1q2W+Rs7C-g>DZYxKh=l1=&`@TvPbEA9@s`$%7+T#Mn38a8pPOhI2 zcm{yLS@(eC90rqa?F&W~D7p9_Mhys+vVz)fhRBDsOZ!6l=z+e7djMDjNRp8Dzmriu zUN8spMCU-jT>HiUs?bLkUGie1)iL$hF|Bs)pKxLTZ zxq0f{mERluxs_?>Z~Df=b$)zzzDpd6btNBY^=fXj?RCs*>k-F*Ztq2{_Cp#|8E+tf*n7DyrcU{m(%<8f0F+H zTBUf@KU>8#8t`{q^*SUNV2s96ezvpx^ALzG-gf_n_uFs&=%NSwkM~IrObfBffTCo; z=od>BImy*txFwE5*f9#=TiCrf9=!nymvJG)K+DLBfB4Z|HL(Pm{9ODgldy7#56}}w zHAtQ(>DPZzQT%EV>WFhx;94K5&+Bn6Difgz_6S$F_=^__KzqmCcH!Oi!=%_W3K(mw zJbt(?@KeQ~KaKX7-aF88bn&-XpA+}@GyN{vxQtArh`jiAd(!2N#LyW{V65)jtbaW# z#+>1Mx(ESyv`>FM7#16M@muu31M`=EbuZP2;8odi2Lg^EUJUT&7SKS!X1al6y>_pv z$QyXcugl(~r%ANC0%bgmzId35Mil6xIyw?$nOV%wLo-qOZm^#RUe<+v?c+YmEj`NX z(=C?AXWNLPgjYQAc7^gZ3mgpCY>bYG$yZv#K7cAZTR#w)sP$>ACkSPW?$&6`DeBy$ znIIA1tL$dXuw+vlC3K)!K*eOTZJ2zZG**FdzGD(5#A5|fwD1PY*PD6HJlCTd#TT9Y z9-~>51i6){U(qX|QKEPybA(CZOQTHhv$F7giV_3Rq5D~|w&b{lDhOImEN!CkY{g~Z z_`uSL$CkY(qeLLI{iRNai(G4|`^6#QjS(6PbO;uY-c_%|G9=wM_NSPN_Nd=)rf4b- zYCrcZa#=c$w~FL;c)?SIHt4(9sEtupf!@F_wKvogDbR{lCJV>UrQ+*qnTq4~3QlwB zs{2&4Pk}oQw#F+M#!52HAP7)UR#R%hL(8i(`I0H2vTErTcKIE341(|!4TOq$tzkyP#=9$&X!nPyriT}}iSQBt|0ld<>)Li(&P&1jl%)2+r z+)0N7I`T9|L1ejM;80oQ$&FQ)p<|?|^Ieic(vILA;Jm{81{#FpJ+p;#hua{NoQrlq zJncyP=}Nbs2* zS|299%q#Iut;Za<-%WV<^`3PZ(2L6JU_63#lXkxkV|d;3u&wZ$)cj$ytoXt4R?}`z zq5mttjMoqDX~=YC2zl@n8S~a58=fDxGO=qC`UYNAGj@1uy6W-dYKlTB3#A3g@2T$ne*c8;Jm!Zp+nMwE zocH_l`Yh)>U&;u)bKYt~#ZgMC{Il*I3K7V98e73mpNcQ5OHKAdvbX%c@hJQ9S6%Bj zMtqj<5i%|rKhbmcW!K{(IikIskIfKX$L;Y(rFaW*#>6_fR_FmUMu!IvPIU&loML}Sy@fwB zPIIkPV)(KBNNlY(xb7t>DihOC$q<*XFu!L16+2TOI{o?$;s5p+pg2+)zGgHc<+~t%nE*^_eEykc*SQ` zIhxCu?Tfe!II%5EvE5`1<|3$C%?#3k zcmaxEg8k1bk4dez29jvYQM_75LeTbi!0HncrnWYwDqFI9G6BAGT3=Me7nUWJocQV6 z1^%hO=z5vnJbuVU&F}FF{h~A{XNRcCV!Frhe&6YoXo6Djp19gY)up}GZR6zbbXT{a zua8t$T-9&igQM`sIHE8|Hme%q5KI-Q4lfH3%KtlI48*6VT<88!e;~6gxit#bOA?_N zB%8~Osk+M1eV_s0R$rr9p6Aykz6-h=AIqbfctoi+KQo6<&64yI=bi(n+0csKV;|xT zS#ZW{70IsIuE1PgXUw4dj&KDRsmM)O;Hzcp&R_>E^K5Acxt|f1!~r1Ti*tX?sjVg1 zZ!077y3MR}fg}dxqE{a~;5RD--YI_PJQF6C*3t;c@%-|)Re)e7^?RHnL^eG@r+nT| z*Wg3PSt$dDw-{SiwX@6qPcE*C;)fO=0lQbZTF@mVA~nFcxAIQtJ7>1PUmy{adHG)R=s2=&&yu- zd1Vb~Dxkyz7uZm-vw59!at~?%yUX9g%BgkxKi|e4a_*DspHgDL{lBFg_%p53HXsvo5Uti}KFr3?kk zFM-Yk@MJjiFSq+|SW9Omo4YudWS8X_svrf8UeHxp z_k$2!a<%kWvlVT6ZGO7l*l}v2xg;A7AEC1u&$ipjh6OIfnP1LXmg}#(xaz=?b&uh2 z^jzh8Mw|JUvNbEJQ=Cg}F|a3?)bpUj;!CHu>T(JvvH58o)p$D3?mDGcCy^cVbvzG! zO<|EWyERXGU9w@|iM4+$0$<~um{%vonapSE_EgoqwLl+>(tC**uWY~Bk1g|d)2*C3 zmo2!ba0P2&4*$Rx)jq&@Idyt9>r@kR)vKjDE+Ta^8lyw3h{ofW6BFX-cP-Et9B;Tr z_8BNCJh+4G=h~9g$NQw#Erpi5_Dds}%GX<#lQCKfGlOtAh^%m_Xs%g;4c67+yaWe#b zTw+tszwSju(8UU-41H1>JQ-yaq|ZFIRsRdk;}0X4+IjA-IP zF;shfRs(#COO}KeO;-mM7%6&ss%w9{AvtZTvdm}9_!A0+$_7(Z%PWkVCTo2%>H{y> zb&GG6)U>^;l!0Ggugz70#O%S+CEUanw$`NqUFD=*g!@@2xFzmKM8+k~CiR3k`ZE>FtAUI*l1!J0_r!ZP!=r#{qgr~uMC zDfr@H>1HA9Ri9cdFSEw*LnkD6h?ON-4i!QBRC)L zTaa0=`b_#z4m0qxbFf1aRywqmu14eMm5$!JYr7|LiVDVH&m&>h*tPR93Ia6(@XHDX zQw~jFyUh8IgDVgmzk!U1cbbpBUN_NU;}Vbc<;rZH3o-@=R+;H-2AB^jn}=+!ngee7 zJSUbf;kG~6>Q1vGm5k=sQ#C>jBO4~rVW{c({0^*~WiwRMjwFT)m#((Io?C{~Yh?W{ z6uU55`zrOvP}H#HwN(^e=XK1`Tn>3arA{#eV2-rD>w(S$sJ8+3lM8)iqi4|Wtklt^ z(*=huUVezipvyIOnR>G+6tiB%%c<<21UFpG@#(3;BRjBcyWXi0AA;Y6)IYR})fCxp zg*}sNDwlR!c8c10wtQSWxxKIOXu-O1a6>}zs-g*C99M&0N}{5df^@gLugO~NDf$Xb z^E~+LQK;^r2~Qe@uBEbFW*$8Q8zK3h!ghS4%Y!>PO!z9)s?*o(?MgQ@0uW_&0g*)% zy})6X+!vTv-V}yX6Wz17*Q}rZqTLGnEs^0ic*ox4*gNu?r_b5cVnjyY`*UrJ z)qmXYm&;D=S~k5PV>l1}yr(}m8?X41QNMHdu(@b!qPIz-fW$_VMSBCGJ%u~^1>)iB>aACLuPC{FC>sbKtWPvrNSE}r8m%}Q zC}%KP2REH$N1EG^$4bd1vkTwrO1%W9w~?j->q$~Kh@dr7Soz*}=>F|?L(QI@599Y8 zbsh|z6_R1CAU1kSM#piwO>~0WHjZM_&NW5%lsSra_dVsW^S)=GW_3rf3@aHFc#Shn zVL$ZvA~UJ&RP3o0c2wDR!s(qu1_`Q}=uKfCPrQXmyQ9Cky{VfGVGY)Qb#sXC0$X6l5!ELajI*i0tc{$yf~5F)6w&$=Sh|#X?Fa+qevk3I>oN z8WKM%#y-F6Fgu1!d>I3IeIT6NpbuQ>faBAc95ALwxXT7u_C#Tfpm>p)-RukB{Rz`Wbr9|xnr zh=sQ*QW;ma>Wn2>E9<`8-1UqTIP37{|3>@ysI)KBL<*@hmO}>Gl5!{zg#gCdzqb80 ztx>W3^Dk!|C^x!z3S$YJ6OQTok1{aQ%VWePFBzBSS?hU>vR$b)y2{>Vat$Ey)Wr6j zY966$q$f?p^r+OSdygr=bX195-_~9QR+K-<$t5Vj3=82)2}PAwP{X=|xy%@1{fcVP zOwG>C{oy?vmSZ%fxK=!@trZtvM|j-4PELlocU3?Ln7(Sp5|;tQSBV6xlv;f*$vk-7 z9&o7dGSh}p^s%5x8`;IKlw!tA-SMK@D)v?R?Tn%_yTJ*S*r%CA0VWT#kIV3-s|GGZ zG3o)1oE%~KwXqEw?!DD6=`DUkqutO^-pRnzg|>#Up@-EKI!o4OnqGYpZB{8r&_rMu z2f%7O>J*Y5H`~UA^L-KlBca<9&n!6k2STymqHV>m~kZ*%IN7Ys6CTpk5RIkC2? zUsC9jFk|ZQ^f?UqBWy^Cy1WhL>J{F+5si{?F%8Jdi6=a=mzH5X^!S7>;S(;0yKHtB zu4X@(9E6CgB}VSObRa-gzfvz_18}XYzXrh@^^B9SB;Ok&$L}idp1$$vUN%rTMODWrKbOi0Gb2KDQ-6g zv(4_0$@&M&^sEf5kTWf|8yzE^5jz}e{sXVoSFo+muMF$r3%07Mey~p?X1pzTEmNCj zDeWzkqC;V9lCCt7ktH5tTPe6{>Vw_$Sf<3NvD@1##lHgCM#;cNcfG*Y;-XMt`>Nda za+@_%9}nkX7k%kHFn6P?rrnAk4enVExBFFkL+5WY5DE9~Q=~=OozBki2E*Bg0)vbu zgSyok4Huj4dPm4%rnHN(bU`Y_#oit`r>Swpt0eY^qkO4Jr&8;B8@#Tz4hxU?{HhEgDX1sdZlw?ozEUOZoua}b4f)XGmwY$}rTnv)x#AzR z6JociXDePm#va^Q)7E&_f%k`qi0qwkxf`ou1}N)eKyxoOwu^)HOJILk3v#MRt!^cqbqlVH-g;i zqBRj<>bV^#VPtCEsIFx@(bmVf^n*F`QRa~n@z;3xN{c`Fj@=KYu-@B0$g%XE8SS2h z?d5F^wZTi!NjnMwB2nR__bU>d@%7@_4I067PU)2T)Vc-ooAbuwqr20|7%4ZIa;NH8 zp5+An!=EFM>D^Y>ll3Z+q~LmkE~LR-FGSF2gj5dBxwqdINbqf~^dx;Il9|dGd1xp6 zd(R8w@ygWr)VqSDj?6ns7bc-JJIbdKHseXd#nog??o%&etHd|9iGR9^7v7we z`PnVJItpYommc9l>e%Sd*S$sT^*gECUe5I#_6Of@+BTH8wSE(Y3v-Yr0)QjiJgnsj z1W#(#H7C(|y4r1rH`E0Sm2Wq<;K4Zd z5YzGP!VcGLQXQ?~Ic@(gb-B0Kbm2|YuB)JJzkXGrOoK2GDMlYw)q(opk5A&@Xs@-A zhW8dR*9XBr3@0T-O9wR& zLZMYi)LuYi&#efJjCg;!?2RioeGa_lZH(fytOq`dBX;>P7qRH{mV4Y5zd*Gbwzj zxLRiuB@b_LrnN%ecTV&{s*Ho53%gH{Hsc_m(wp@VwSK^d5iCWo6Ug8eq%P z4^`4Q+h*2)b%jEYdqze@M=wcdwuKcRuEMs45eiety4oCwmMuKcQHG}i?z8B)DoUFk zDW0m;u+if|FG7h=Fw0c1mG-3m{_sQ+-uN)~S6k;JoBmLaA@y7PEfg5n+qNA-AZPdn ztY0zJ%9FJGf~d8x{(*$H?AtD@&z~tX1X7|bI}L|x|NDD#Yl^mko41xg@R4*5WLLkQ zr|YB)bgO05(7>osi5aI}>HnXulE_fthrq3te+Jxt&%XTMNdku7=+9p>^4G-vn-KW) ze`+22|Mx|~|7I`17<$luhrz#}$A(g#dd>=fQwO)~*8MraiO=$s7w#lFc%9i6;RYw# zbpLW*2cTSOFO+BQVAxPl{_(77oFLe%DyffgcgJu>Pm{u&_%_4G_9gL>2ZAb{cyi6R zK=>Xr3Mzz2zwvYnSS0aeQ;6)=G+NRkxeLHx{Z>i&USI(x~H-H`#W_CEvcpz1%`h@v47c{+SjxREOzG`8L z^GNI201nO2{G%zRoGrgk{zzfrt#Z{azj=IGh2yvaddkmT2Ilz+=W5%60 zy2{-2;((__{9W4HkPtH>k9vcs>mycgV8z9HOda8ITv4gf%MlwkpVxb0ZKf9cIT$%M zRyz}S>aGo3hQvKt%{&yXR$tA$!~M2`5kYiiYToHbOEryNUNOlIrQ40PBW`IX?W9FO zvjg#xU!H4=`a3JGAGh^0MQSXY-#2jM1wiqGTm1~dP1J~ujkdk!jpks=;6|v8JCpK^ zzuw*_DPNplxEn%8d z_ia4i5nYPZJ_i};nz9i373b`D)v`bAqG2q!e>6>{Hf=IX_G>k%wVO;XE$Mu8*X;c1 z20bfvWJ-wclkkRl_L(fUP}6cL#*f(I!{ z!u!bwo`G38Vdl};CUJwo^#NGllBIYmNqslyMCTkE+bdIpQM$FflHQl#QH!F6Yiz95 zwn8;(vQ^!w|E1l)_jz8b#n$V--SA@d^^yAq(|e&rL3c5z zqI&~%hjX2lx$e7qHU(^O{@GP?4p%~uIEQ~DKj;?Z>KEQQb!hyoz##uv%(e=T!924u zAN@-R4tTRbgB`!#o{{<1jQ^UIyr~6S#NaHx*vjSeUsZ-yF|C} z2EBe#M>R~`GRz_>!~+@R_uBgSmWm?w)7M?*`c7);fdshty~wJ8$S&AGkRackB>{ zxEIdc?>ZmM@Xlae!>R$-ZcS0Cx#aQ5+ENM5gA}W@ zKe^R%jM@z=ZvBx&dBifgm+2ect?|8lwhi7Qa=6f)b$Orw*-J5*N&5_febTe zMtPa+zLPIaz;tSPhj8%efbL)7i*#z27e23u)te5)TSUaBvB}1=f|E+NeT_ssZ$1T* z9H>`vZhUL#x;SN$|1qqu+wbx|q z>$);j5}hU^Nmp!F?1)uLgslPCAPxQywee-A!j+@xgH03U4Ye35p6MW#Jm1n5?L5w%GrY??-a>UZDn zNlT|_x0SN09aIM>x8L5@nMR}^`j2dl!cj3mwQN^Bi=_NeDWOI0O2JqsHV1xXCg1PO zq(x>iXK!Il^V3@Po3+l{x2<*0F8dw1?Zg}`hrbY?9WX=hk{Kv>30{f{X2e>SIEC zwEZY4TCs)!U1_QH-TFKdk=~0f+eS}pQPx==izlp<{#;M-u?7~Ui{AljE0W6VW%1Rom23_w?}b>6u?aL8rbs>@l}ltV`?LbeDq4 zXTLt+IoC#^)kxAZe~A*H&R!l8vW+kL4MN}{UYBtNQ2H%z&9+?<&b!o-9@YmUc%Zrl z?dhvWE!@YX%2!*sJFR)vpCPFWit-|ijtL89;=;A`%-W>RSY$DaV;SSto?SCTpyNs z#$Qw0OjFmcV2Yy0m00{#upUYcb7z07WYiz@8bg0D+-Qn0unt~%4&wTe+zEQd^>6fR z`L7|{Pthnp-|Wgj$uyd=*kmW@@FEcNK>ux_vjitpaXl#0rh&zQ#l@rX>+E+ms`W_2 zr4QbqHNo2#{g2;{ZLe_PYU(}h;IV$pKFSN;MXU|pkI6*nJ*{11Uv<$VAE literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/workbench-1-manage-drivers.png b/docs/reference/images/sql/client-apps/workbench-1-manage-drivers.png new file mode 100644 index 0000000000000000000000000000000000000000..e305fd2a9dda239e6ca88d8e3ebd6d1279b96f21 GIT binary patch literal 16439 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 literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/workbench-2-add-driver.png b/docs/reference/images/sql/client-apps/workbench-2-add-driver.png new file mode 100644 index 0000000000000000000000000000000000000000..03e740f400ae10fa13ec39d458cc936a5ca3e08f GIT binary patch literal 26008 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 literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/workbench-3-connection.png b/docs/reference/images/sql/client-apps/workbench-3-connection.png new file mode 100644 index 0000000000000000000000000000000000000000..32643375e3de9f483e377feb6b54c63b1766646f GIT binary patch literal 35138 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